8000 diff --git a/.appveyor.yml b/.appveyor.yml index 4b417c5dfc122..dfc01ccce97de 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,16 +2,12 @@ build: false clone_depth: 2 clone_folder: c:\projects\symfony -cache: - - composer.phar - - .phpunit -> phpunit - init: - SET PATH=c:\php;%PATH% - SET COMPOSER_NO_INTERACTION=1 - SET SYMFONY_DEPRECATIONS_HELPER=strict - - SET "SYMFONY_REQUIRE=>=4.2" - SET ANSICON=121x90 (121x90) + - SET SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE=1 - REG ADD "HKEY_CURRENT_USER\Software\Microsoft\Command Processor" /v DelayedExpansion /t REG_DWORD /d 1 /f install: @@ -19,20 +15,26 @@ install: - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php-7.1.3-Win32-VC14-x86.zip - 7z x php-7.1.3-Win32-VC14-x86.zip -y >nul - cd ext - - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.8-7.1-ts-vc14-x86.zip - - 7z x php_apcu-5.1.8-7.1-ts-vc14-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_apcu-5.1.18-7.1-ts-vc14-x86.zip + - 7z x php_apcu-5.1.18-7.1-ts-vc14-x86.zip -y >nul + - appveyor DownloadFile https://github.com/symfony/binary-utils/releases/download/v0.1/php_redis-5.1.1-7.1-ts-vc14-x86.zip + - 7z x php_redis-5.1.1-7.1-ts-vc14-x86.zip -y >nul - cd .. - copy /Y php.ini-development php.ini-min - echo memory_limit=-1 >> php.ini-min - echo serialize_precision=14 >> php.ini-min - echo max_execution_time=1200 >> php.ini-min + - echo post_max_size=4G >> php.ini-min + - echo upload_max_filesize=4G >> php.ini-min - echo date.timezone="America/Los_Angeles" >> php.ini-min - echo extension_dir=ext >> php.ini-min + - echo extension=php_xsl.dll >> php.ini-min - copy /Y php.ini-min php.ini-max - echo zend_extension=php_opcache.dll >> php.ini-max - echo opcache.enable_cli=1 >> php.ini-max - echo extension=php_openssl.dll >> php.ini-max - echo extension=php_apcu.dll >> php.ini-max + - echo extension=php_redis.dll >> php.ini-max - echo apc.enable_cli=1 >> php.ini-max - echo extension=php_intl.dll >> php.ini-max - echo extension=php_mbstring.dll >> php.ini-max @@ -41,21 +43,23 @@ install: - echo extension=php_curl.dll >> php.ini-max - copy /Y php.ini-max php.ini - cd c:\projects\symfony - - IF NOT EXIST composer.phar (appveyor DownloadFile https://github.com/composer/composer/releases/download/1.7.1/composer.phar) - - php composer.phar self-update - - copy /Y .composer\* %APPDATA%\Composer\ - - php composer.phar global require --no-progress --no-scripts --no-plugins symfony/flex dev-master - - php .github/build-packages.php "HEAD^" src\Symfony\Bridge\PhpUnit src\Symfony\Contracts - - IF %APPVEYOR_REPO_BRANCH%==master (SET COMPOSER_ROOT_VERSION=dev-master) ELSE (SET COMPOSER_ROOT_VERSION=%APPVEYOR_REPO_BRANCH%.x-dev) - - php composer.phar update --no-progress --no-suggest --ansi + - appveyor DownloadFile https://getcomposer.org/download/latest-2.2.x/composer.phar + - mkdir %APPDATA%\Composer && copy /Y .github\composer-config.json %APPDATA%\Composer\config.json + - git config --global user.email "" + - git config --global user.name "Symfony" + - FOR /F "tokens=* USEBACKQ" %%F IN (`bash -c "grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -o '[0-9][0-9]*\.[0-9]'"`) DO (SET SYMFONY_VERSION=%%F) + - php .github/build-packages.php HEAD^ %SYMFONY_VERSION% src\Symfony\Bridge\PhpUnit + - SET COMPOSER_ROOT_VERSION=%SYMFONY_VERSION%.x-dev + - php composer.phar update --no-progress --ansi - php phpunit install + - choco install memurai-developer test_script: - SET X=0 - SET SYMFONY_PHPUNIT_SKIPPED_TESTS=phpunit.skipped - copy /Y c:\php\php.ini-min c:\php\php.ini - - IF %APPVEYOR_REPO_BRANCH% neq master (rm -Rf src\Symfony\Bridge\PhpUnit) - - php phpunit src\Symfony --exclude-group benchmark,intl-data || SET X=!errorlevel! + - IF %APPVEYOR_REPO_BRANCH:~-2% neq .x (rm -Rf src\Symfony\Bridge\PhpUnit) + - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data,network,transient-on-windows || SET X=!errorlevel! - copy /Y c:\php\php.ini-max c:\php\php.ini - - php phpunit src\Symfony --exclude-group benchmark,intl-data || SET X=!errorlevel! + - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data,network,transient-on-windows || SET X=!errorlevel! - exit %X% diff --git a/.composer/config.json b/.composer/config.json deleted file mode 100644 index 941bc3b56e8cd..0000000000000 --- a/.composer/config.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "config": { - "preferred-install": { - "*": "dist" - } - } -} diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000..c255f66722075 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/src/Symfony/Contracts export-ignore +/src/Symfony/Bridge/PhpUnit export-ignore diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e459d1e55f616..f6d70f0581cc3 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,10 +1,13 @@ # Console +/src/Symfony/Component/Console/ @chalasr /src/Symfony/Component/Console/Logger/ConsoleLogger.php @dunglas # DependencyInjection /src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php @dunglas +# ErrorHandler +/src/Symfony/Component/ErrorHandler/ @yceruto # Form /src/Symfony/Bridge/Twig/Extension/FormExtension.php @xabbuh -/src/Symfony/Bridge/Twig/Form/* @xabbuh +/src/Symfony/Bridge/Twig/Form/ @xabbuh /src/Symfony/Bridge/Twig/Node/FormThemeNode.php @xabbuh /src/Symfony/Bridge/Twig/Node/RenderBlockNode.php @xabbuh /src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php @xabbuh @@ -13,34 +16,38 @@ /src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php @xabbuh /src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php @xabbuh /src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/FormPass.php @xabbuh -/src/Symfony/Bundle/FrameworkBundle/Resources/views/* @xabbuh +/src/Symfony/Bundle/FrameworkBundle/Resources/views/ @xabbuh /src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @xabbuh /src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/FormPassTest.php @xabbuh /src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php @xabbuh /src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php @xabbuh -/src/Symfony/Component/Form/* @xabbuh +/src/Symfony/Component/Form/ @xabbuh @yceruto # HttpKernel /src/Symfony/Component/HttpKernel/Log/Logger.php @dunglas -# LDAP -/src/Symfony/Component/Ldap/* @csarrazi # Lock -/src/Symfony/Component/Lock/* @jderusse -# Messenger -/src/Symfony/Bridge/Doctrine/Messenger/* @sroze -/src/Symfony/Component/Messenger/* @sroze +/src/Symfony/Component/Lock/ @jderusse +# OptionsResolver +/src/Symfony/Component/OptionsResolver/ @yceruto # PropertyInfo -/src/Symfony/Component/PropertyInfo/* @dunglas -/src/Symfony/Bridge/Doctrine/PropertyInfo/* @dunglas +/src/Symfony/Component/PropertyInfo/ @dunglas +/src/Symfony/Bridge/Doctrine/PropertyInfo/ @dunglas # Serializer -/src/Symfony/Component/Serializer/* @dunglas +/src/Symfony/Component/Serializer/ @dunglas +# Security +/src/Symfony/Bridge/Doctrine/Security/ @wouterj @chalasr +/src/Symfony/Bundle/SecurityBundle/ @wouterj @chalasr +/src/Symfony/Component/Security/ @wouterj @chalasr +/src/Symfony/Component/Ldap/Security/ @wouterj @chalasr +# TwigBundle +/src/Symfony/Bundle/TwigBundle/ @yceruto # WebLink -/src/Symfony/Component/WebLink/* @dunglas +/src/Symfony/Component/WebLink/ @dunglas # Workflow /src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @lyrixx /src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php @lyrixx /src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @lyrixx /src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ValidateWorkflowsPass.php @lyrixx /src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/WorkflowGuardListenerPass.php @lyrixx -/src/Symfony/Component/Workflow/* @lyrixx +/src/Symfony/Component/Workflow/ @lyrixx # Yaml -/src/Symfony/Component/Yaml/* @xabbuh +/src/Symfony/Component/Yaml/ @xabbuh diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md deleted file mode 100644 index 16e2603b76a1d..0000000000000 --- a/.github/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,8 +0,0 @@ -# Code of Conduct - -This project follows a [Code of Conduct][code_of_conduct] in order to ensure an open and welcoming environment. -Please read the full text for understanding the accepted and unaccepted behavior. -Please read also the [reporting guidelines][guidelines], in case you encountered or witnessed any misbehavior. - -[code_of_conduct]: https://symfony.com/doc/current/contributing/code_of_conduct/index.html -[guidelines]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md deleted file mode 100644 index 4a64e16edf0a5..0000000000000 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -name: πŸ› Bug Report -about: Report errors and problems - ---- - -**Symfony version(s) affected**: x.y.z - -**Description** - - -**How to reproduce** - - -**Possible Solution** - - -**Additional context** - diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.yaml b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml new file mode 100644 index 0000000000000..ef0f72c794278 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.yaml @@ -0,0 +1,44 @@ +name: πŸ› Bug Report +description: ⚠️ NEVER report security issues, read https://symfony.com/security instead +labels: Bug + +body: + - type: input + id: affected-versions + attributes: + label: Symfony version(s) affected + placeholder: x.y.z + validations: + required: true + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the problem + validations: + required: true + - type: textarea + id: how-to-reproduce + attributes: + label: How to reproduce + description: | + ⚠️ This is the most important part of the report ⚠️ + Without a way to easily reproduce your issue, there is little chance we will be able to help you and work on a fix. + Please, take the time to show us some code and/or config that is needed for others to reproduce the problem easily. + Most of the time, creating a "bug reproducer" as explained in the URL below is the best way to help us + and increases the chances someone will have a look at it: + https://symfony.com/doc/current/contributing/code/reproducer.html + validations: + required: true + - type: textarea + id: possible-solution + attributes: + label: Possible Solution + description: | + Optional: only if you have suggestions on a fix/reason for the bug + Don't hesitate to create a pull request with your solution, it helps get faster feedback. + - type: textarea + id: additional-context + attributes: + label: Additional Context + description: "Optional: any other context about the problem: log messages, screenshots, etc." diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.md b/.github/ISSUE_TEMPLATE/2_Feature_request.md deleted file mode 100644 index 335321e413607..0000000000000 --- a/.github/ISSUE_TEMPLATE/2_Feature_request.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: πŸš€ Feature Request -about: RFC and ideas for new features and improvements - ---- - -**Description** - - -**Example** - diff --git a/.github/ISSUE_TEMPLATE/2_Feature_request.yaml b/.github/ISSUE_TEMPLATE/2_Feature_request.yaml new file mode 100644 index 0000000000000..bd300eb1e82b2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/2_Feature_request.yaml @@ -0,0 +1,17 @@ +name: πŸš€ Feature Request +description: RFC and ideas for new features and improvements +body: + - type: textarea + id: description + attributes: + label: Description + description: A clear and concise description of the new feature + validations: + required: true + - type: textarea + id: example + attributes: + label: Example + description: | + A simple example of the new feature in action (include PHP code, YAML config, etc.) + If the new feature changes an existing feature, include a simple before/after comparison. diff --git a/.github/ISSUE_TEMPLATE/3_Support_question.md b/.github/ISSUE_TEMPLATE/3_Support_question.md deleted file mode 100644 index 9480710c15655..0000000000000 --- a/.github/ISSUE_TEMPLATE/3_Support_question.md +++ /dev/null @@ -1,11 +0,0 @@ ---- -name: β›” Support Question -about: See https://symfony.com/support for questions about using Symfony and its components - ---- - -We use GitHub issues only to discuss about Symfony bugs and new features. For -this kind of questions about using Symfony or third-party bundles, please use -any of the support alternatives shown in https://symfony.com/support - -Thanks! diff --git a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md b/.github/ISSUE_TEMPLATE/4_Documentation_issue.md deleted file mode 100644 index 0855c3c5f1e12..0000000000000 --- a/.github/ISSUE_TEMPLATE/4_Documentation_issue.md +++ /dev/null @@ -1,10 +0,0 @@ ---- -name: β›” Documentation Issue -about: See https://github.com/symfony/symfony-docs/issues for documentation issues - ---- - -Symfony Documentation has its own dedicated repository. Please open your -documentation-related issue at https://github.com/symfony/symfony-docs/issues - -Thanks! diff --git a/.github/ISSUE_TEMPLATE/5_Security_issue.md b/.github/ISSUE_TEMPLATE/5_Security_issue.md deleted file mode 100644 index 9b3165eb1db47..0000000000000 --- a/.github/ISSUE_TEMPLATE/5_Security_issue.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: β›” Security Issue -about: See https://symfony.com/security to report security-related issues - ---- - -⚠ PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, SEE BELOW. - -If you have found a security issue in Symfony, please send the details to -security [at] symfony.com and don't disclose it publicly until we can provide a -fix for it. - -More information: https://symfony.com/security diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000000000..34227566ed84a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Support Question + url: https://symfony.com/support + about: We use GitHub issues only to discuss about Symfony bugs and new features. For this kind of questions about using Symfony or third-party bundles, please use any of the support alternatives shown in https://symfony.com/support + - name: Documentation Issue + url: https://github.com/symfony/symfony-docs/issues + about: Symfony Documentation has its own dedicated repository. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 3d686194101c0..30408a440624e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,22 +1,22 @@ | Q | A | ------------- | --- -| Branch? | 4.4 for features / 3.4, 4.2 or 4.3 for bug fixes +| Branch? | 6.3 for features / 4.4, 5.4, 6.0, 6.1, or 6.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no -| BC breaks? | no | Deprecations? | yes/no -| Tests pass? | yes -| Fixed tickets | #... +| Tickets | Fix #... | License | MIT | Doc PR | symfony/symfony-docs#... - diff --git a/.github/SECURITY.md b/.github/SECURITY.md deleted file mode 100644 index 60990950bf039..0000000000000 --- a/.github/SECURITY.md +++ /dev/null @@ -1,10 +0,0 @@ -Security Policy -=============== - -If you found any issues that might have security implications, -please send a report to security[at]symfony.com -DO NOT PUBLISH SECURITY REPORTS PUBLICLY. - -The full [Security Policy][1] is described in the official documentation. - - [1]: https://symfony.com/security diff --git a/.github/build-packages.php b/.github/build-packages.php index 5e9bcc141544e..30dcf0c9adce8 100644 --- a/.github/build-packages.php +++ b/.github/build-packages.php @@ -1,22 +1,19 @@ $_SERVER['argc']) { - echo "Usage: branch dir1 dir2 ... dirN\n"; + echo "Usage: branch version dir1 dir2 ... dirN\n"; exit(1); } chdir(dirname(__DIR__)); -$json = ltrim(file_get_contents('composer.json')); -if ($json !== $package = preg_replace('/\n "repositories": \[\n.*?\n \],/s', '', $json)) { - file_put_contents('composer.json', $package); -} - $dirs = $_SERVER['argv']; array_shift($dirs); $mergeBase = trim(shell_exec(sprintf('git merge-base "%s" HEAD', array_shift($dirs)))); +$version = array_shift($dirs); -$packages = array(); +$packages = []; $flags = JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; +$preferredInstall = json_decode(file_get_contents(__DIR__.'/composer-config.json'), true)['config']['preferred-install']; foreach ($dirs as $k => $dir) { if (!system("git diff --name-only $mergeBase -- $dir", $exitStatus)) { @@ -34,35 +31,32 @@ exit(1); } - $package->repositories = array(array( + $package->repositories = [[ 'type' => 'composer', 'url' => 'file://'.str_replace(DIRECTORY_SEPARATOR, '/', dirname(__DIR__)).'/', - )); + ]]; if (false === strpos($json, "\n \"repositories\": [\n")) { - $json = rtrim(json_encode(array('repositories' => $package->repositories), $flags), "\n}").','.substr($json, 1); + $json = rtrim(json_encode(['repositories' => $package->repositories], $flags), "\n}").','.substr($json, 1); file_put_contents($dir.'/composer.json', $json); } - passthru("cd $dir && tar -cf package.tar --exclude='package.tar' *"); - if (!isset($package->extra->{'branch-alias'}->{'dev-master'})) { - echo "Missing \"dev-master\" branch-alias in composer.json extra.\n"; - exit(1); + if (isset($preferredInstall[$package->name]) && 'source' === $preferredInstall[$package->name]) { + passthru("cd $dir && tar -cf package.tar --exclude='package.tar' *"); + } else { + passthru("cd $dir && git init && git add . && git commit -q -m - && git archive -o package.tar HEAD && rm .git/ -Rf"); } - $package->version = str_replace('-dev', '.x-dev', $package->extra->{'branch-alias'}->{'dev-master'}); + + $package->version = preg_replace('/(?:\.x)?-dev$/', '', $package->extra->{'branch-alias'}->{'dev-main'} ?? $version).'.x-dev'; $package->dist['type'] = 'tar'; $package->dist['url'] = 'file://'.str_replace(DIRECTORY_SEPARATOR, '/', dirname(__DIR__))."/$dir/package.tar"; $packages[$package->name][$package->version] = $package; - $versions = @file_get_contents('https://repo.packagist.org/p/'.$package->name.'.json') ?: sprintf('{"packages":{"%s":{"dev-master":%s}}}', $package->name, file_get_contents($dir.'/composer.json')); + $versions = @file_get_contents('https://repo.packagist.org/p/'.$package->name.'.json') ?: sprintf('{"packages":{"%s":{"%s":%s}}}', $package->name, $package->version, file_get_contents($dir.'/composer.json')); $versions = json_decode($versions)->packages->{$package->name}; - if (isset($versions->{'dev-master'}) && $package->version === str_replace('-dev', '.x-dev', $versions->{'dev-master'}->extra->{'branch-alias'}->{'dev-master'})) { - unset($versions->{'dev-master'}); - } - foreach ($versions as $v => $package) { - $packages[$package->name] += array($v => $package); + $packages[$package->name] += [$v => $package]; } } @@ -75,10 +69,12 @@ exit(1); } - $package->repositories = array(array( + $package->repositories[] = [ 'type' => 'composer', 'url' => 'file://'.str_replace(DIRECTORY_SEPARATOR, '/', dirname(__DIR__)).'/', - )); - $json = rtrim(json_encode(array('repositories' => $package->repositories), $flags), "\n}").','.substr($json, 1); + ]; + + $json = preg_replace('/\n "repositories": \[\n.*?\n \],/s', '', $json); + $json = rtrim(json_encode(['repositories' => $package->repositories], $flags), "\n}").','.substr($json, 1); file_put_contents('composer.json', $json); } diff --git a/.github/composer-config.json b/.github/composer-config.json new file mode 100644 index 0000000000000..bf796b6b3d85a --- /dev/null +++ b/.github/composer-config.json @@ -0,0 +1,15 @@ +{ + "config": { + "platform-check": false, + "preferred-install": { + "symfony/form": "source", + "symfony/http-kernel": "source", + "symfony/proxy-manager-bridge": "source", + "symfony/validator": "source", + "*": "dist" + }, + "allow-plugins": { + "symfony/flex": true + } + } +} diff --git a/.github/patch-types.php b/.github/patch-types.php new file mode 100644 index 0000000000000..a714a3370358d --- /dev/null +++ b/.github/patch-types.php @@ -0,0 +1,57 @@ +addClassMap(['Symfony\Component\Debug\Exception\FlattenException' => \dirname(__DIR__).'/src/Symfony/Component/Debug/Exception/FlattenException.php']); + +return $loader; + +EOTXT +, file_get_contents(__DIR__.'/../vendor/autoload.php'))); + +$loader = require __DIR__.'/../vendor/autoload.php'; + +Symfony\Component\ErrorHandler\DebugClassLoader::enable(); + +foreach ($loader->getClassMap() as $class => $file) { + switch (true) { + case false !== strpos($file = realpath($file), '/vendor/'): + case false !== strpos($file, '/src/Symfony/Bridge/PhpUnit/'): + case false !== strpos($file, '/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Validation/Article.php'): + case false !== strpos($file, '/src/Symfony/Component/Cache/Tests/Fixtures/DriverWrapper.php'): + case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/BadFileName.php'): + case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/BadParent.php'): + case false !== strpos($file, '/src/Symfony/Component/Config/Tests/Fixtures/ParseError.php'): + case false !== strpos($file, '/src/Symfony/Component/Debug/Tests/Fixtures/'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/compositetype_classes.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/intersectiontype_classes.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/MultipleArgumentsOptionalScalarNotReallyOptional.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/CheckTypeDeclarationsPass/IntersectionConstructor.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ParentNotExists.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Preload/'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'): + case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/ParentDummy.php'): + case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Php81Dummy.php'): + case false !== strpos($file, '/src/Symfony/Component/Serializer/Tests/Normalizer/Features/ObjectOuter.php'): + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/NotLoadableClass.php'): + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/ReflectionIntersectionTypeFixture.php'): + case false !== strpos($file, '/src/Symfony/Component/VarDumper/Tests/Fixtures/ReflectionUnionTypeWithIntersectionFixture.php'): + continue 2; + } + + class_exists($class); +} + +Symfony\Component\ErrorHandler\DebugClassLoader::checkClasses(); diff --git a/.github/psalm/.gitignore b/.github/psalm/.gitignore new file mode 100644 index 0000000000000..53021ab087be4 --- /dev/null +++ b/.github/psalm/.gitignore @@ -0,0 +1,4 @@ +* +!.gitignore +!stubs +!stubs/* diff --git a/.github/psalm/psalm.baseline.xml b/.github/psalm/psalm.baseline.xml new file mode 100644 index 0000000000000..f74693accd46f --- /dev/null +++ b/.github/psalm/psalm.baseline.xml @@ -0,0 +1,3 @@ + + + diff --git a/.github/psalm/stubs/ForwardCompatTestTrait.php b/.github/psalm/stubs/ForwardCompatTestTrait.php new file mode 100644 index 0000000000000..e3ddf4da3d431 --- /dev/null +++ b/.github/psalm/stubs/ForwardCompatTestTrait.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\Test; + +use PHPUnit\Framework\TestCase; + +/** + * @internal + */ +trait ForwardCompatTestTrait +{ + private function doSetUp(): void + { + } + + private function doTearDown(): void + { + } + + protected function setUp(): void + { + $this->doSetUp(); + } + + protected function tearDown(): void + { + $this->doTearDown(); + } +} diff --git a/.github/psalm/stubs/SetUpTearDownTrait.php b/.github/psalm/stubs/SetUpTearDownTrait.php new file mode 100644 index 0000000000000..20dbe6fe73e0f --- /dev/null +++ b/.github/psalm/stubs/SetUpTearDownTrait.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\Bridge\PhpUnit; + +use PHPUnit\Framework\TestCase; + +trait SetUpTearDownTrait +{ + use Legacy\SetUpTearDownTraitForV8; +} diff --git a/.github/rm-invalid-lowest-lock-files.php b/.github/rm-invalid-lowest-lock-files.php deleted file mode 100644 index c036fd356f045..0000000000000 --- a/.github/rm-invalid-lowest-lock-files.php +++ /dev/null @@ -1,158 +0,0 @@ - array(), 'packages-dev' => array()); - $composerJsons[$composerJson['name']] = array($dir, $composerLock['packages'] + $composerLock['packages-dev'], getRelevantContent($composerJson)); -} - -$referencedCommits = array(); - -foreach ($composerJsons as list($dir, $lockedPackages)) { - foreach ($lockedPackages as $lockedJson) { - if (0 !== strpos($version = $lockedJson['version'], 'dev-') && '-dev' !== substr($version, -4)) { - continue; - } - - if (!isset($composerJsons[$name = $lockedJson['name']])) { - echo "$dir/composer.lock references missing $name.\n"; - @unlink($dir.'/composer.lock'); - continue 2; - } - - if (isset($composerJsons[$name][2]['repositories']) && !isset($lockedJson['repositories'])) { - 8000 // the locked package has been patched locally but the lock references a commit, - // which means the referencing package itself is not modified - continue; - } - - foreach (array('minimum-stability', 'prefer-stable') as $key) { - if (array_key_exists($key, $composerJsons[$name][2])) { - $lockedJson[$key] = $composerJsons[$name][2][$key]; - } - } - - // use weak comparison to ignore ordering - if (getRelevantContent($lockedJson) != $composerJsons[$name][2]) { - echo "$dir/composer.lock is not in sync with $name.\n"; - @unlink($dir.'/composer.lock'); - continue 2; - } - - if ($lockedJson['dist']['reference']) { - $referencedCommits[$name][$lockedJson['dist']['reference']][] = $dir; - } - } -} - -if (!$referencedCommits) { - return; -} - -@mkdir($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org', 0777, true); - -$ch = null; -$mh = curl_multi_init(); -$sh = curl_share_init(); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_COOKIE); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_DNS); -curl_share_setopt($sh, CURLSHOPT_SHARE, CURL_LOCK_DATA_SSL_SESSION); -$chs = array(); - -foreach ($referencedCommits as $name => $dirsByCommit) { - $chs[] = $ch = array(curl_init(), fopen($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org/provider-'.strtr($name, '/', '$').'.json', 'wb')); - curl_setopt($ch[0], CURLOPT_URL, 'https://repo.packagist.org/p/'.$name.'.json'); - curl_setopt($ch[0], CURLOPT_FILE, $ch[1]); - curl_setopt($ch[0], CURLOPT_SHARE, $sh); - curl_multi_add_handle($mh, $ch[0]); -} - -do { - curl_multi_exec($mh, $active); - curl_multi_select($mh); -} while ($active); - -foreach ($chs as list($ch, $fd)) { - curl_multi_remove_handle($mh, $ch); - curl_close($ch); - fclose($fd); -} - -foreach ($referencedCommits as $name => $dirsByCommit) { - $repo = file_get_contents($_SERVER['HOME'].'/.cache/composer/repo/https---repo.packagist.org/provider-'.strtr($name, '/', '$').'.json'); - $repo = json_decode($repo, true); - - foreach ($repo['packages'][$name] as $version) { - unset($referencedCommits[$name][$version['source']['reference']]); - } -} - -foreach ($referencedCommits as $name => $dirsByCommit) { - foreach ($dirsByCommit as $dirs) { - foreach ($dirs as $dir) { - if (file_exists($dir.'/composer.lock')) { - echo "$dir/composer.lock references old commit for $name.\n"; - @unlink($dir.'/composer.lock'); - } - } - } -} diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml new file mode 100644 index 0000000000000..5cd8a425eb58a --- /dev/null +++ b/.github/workflows/integration-tests.yml @@ -0,0 +1,146 @@ +name: Integration + +on: + push: + pull_request: + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + + tests: + name: Tests + runs-on: Ubuntu-20.04 + + strategy: + matrix: + php: ['7.1', '8.0'] + + services: + ldap: + image: bitnami/openldap + ports: + - 3389:3389 + env: + LDAP_ADMIN_USERNAME: admin + LDAP_ADMIN_PASSWORD: symfony + LDAP_ROOT: dc=symfony,dc=com + LDAP_PORT_NUMBER: 3389 + LDAP_USERS: a + LDAP_PASSWORDS: a + redis: + image: redis:6.0.0 + ports: + - 16379:6379 + redis-cluster: + image: grokzen/redis-cluster:5.0.4 + ports: + - 7000:7000 + - 7001:7001 + - 7002:7002 + - 7003:7003 + - 7004:7004 + - 7005:7005 + - 7006:7006 + env: + STANDALONE: 1 + redis-sentinel: + image: bitnami/redis-sentinel:6.0 + ports: + - 26379:26379 + env: + REDIS_MASTER_HOST: redis + REDIS_MASTER_SET: redis_sentinel + REDIS_SENTINEL_QUORUM: 1 + memcached: + image: memcached:1.6.5 + ports: + - 11211:11211 + rabbitmq: + image: rabbitmq:3.8.3 + ports: + - 5672:5672 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install system dependencies + run: | + echo "::group::apt-get update" + sudo apt-get update + echo "::endgroup::" + + echo "::group::install tools & libraries" + sudo apt-get install redis-server + sudo -- sh -c 'echo unixsocket /var/run/redis/redis-server.sock >> /etc/redis/redis.conf' + sudo -- sh -c 'echo unixsocketperm 777 >> /etc/redis/redis.conf' + sudo service redis-server restart + echo "::endgroup::" + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + extensions: "memcached,redis-5.3.4,xsl,ldap" + ini-values: date.timezone=UTC,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1 + php-version: "${{ matrix.php }}" + + - name: Load fixtures + uses: docker://bitnami/openldap + with: + entrypoint: /bin/bash + args: -c "(/opt/bitnami/openldap/bin/ldapwhoami -H ldap://ldap:3389 -D cn=admin,dc=symfony,dc=com -w symfony||sleep 5) && /opt/bitnami/openldap/bin/ldapadd -H ldap://ldap:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif && /opt/bitnami/openldap/bin/ldapdelete -H ldap://ldap:3389 -D cn=admin,dc=symfony,dc=com -w symfony cn=a,ou=users,dc=symfony,dc=com" + + - name: Install dependencies + run: | + COMPOSER_HOME="$(composer config home)" + ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" + export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev + echo COMPOSER_ROOT_VERSION=$COMPOSER_ROOT_VERSION >> $GITHUB_ENV + + echo "::group::composer update" + composer update --no-progress --ansi + echo "::endgroup::" + + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + + - name: Run tests + run: ./phpunit --group integration -v + env: + REDIS_HOST: 'localhost:16379' + REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' + REDIS_SENTINEL_HOSTS: 'localhost:26379' + REDIS_SENTINEL_SERVICE: redis_sentinel + MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages + MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages + + #- name: Run HTTP push tests + # if: matrix.php == '8.0' + # run: | + # [ -d .phpunit ] && mv .phpunit .phpunit.bak + # wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz && mv vulcain /usr/local/bin + # docker run --rm -e COMPOSER_ROOT_VERSION -v $(pwd):/app -v $(which composer):/usr/local/bin/composer -v $(which vulcain):/usr/local/bin/vulcain -w /app php:8.0-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push + # sudo rm -rf .phpunit + # [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit + + - uses: marceloprado/has-changed-path@v1 + id: changed-translation-files + with: + paths: src/**/Resources/translations/*.xlf + + - name: Check Translation Status + if: steps.changed-translation-files.outputs.changed == 'true' + run: | + php src/Symfony/Component/Translation/Resources/bin/translation-status.php -v diff --git a/.github/workflows/intl-data-tests.yml b/.github/workflows/intl-data-tests.yml new file mode 100644 index 0000000000000..fef1dd1140374 --- /dev/null +++ b/.github/workflows/intl-data-tests.yml @@ -0,0 +1,73 @@ +name: Intl data + +on: + push: + paths: + - 'src/Symfony/Component/Intl/Resources/data/**' + pull_request: + paths: + - 'src/Symfony/Component/Intl/Resources/data/**' + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + tests: + name: Tests + runs-on: Ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Install system dependencies + run: | + echo "::group::apt-get update" + sudo apt-get update + echo "::endgroup::" + + echo "::group::install tools & libraries" + sudo apt-get install icu-devtools + echo "::endgroup::" + + - name: Define the ICU version + run: | + SYMFONY_ICU_VERSION=$(php -r 'require "src/Symfony/Component/Intl/Intl.php"; echo Symfony\Component\Intl\Intl::getIcuStubVersion();') + echo "SYMFONY_ICU_VERSION=$SYMFONY_ICU_VERSION" >> $GITHUB_ENV + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + extensions: "zip,intl-${{env.SYMFONY_ICU_VERSION}}" + ini-values: "memory_limit=-1" + php-version: "7.4" + + - name: Install dependencies + run: | + COMPOSER_HOME="$(composer config home)" + ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" + export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev + echo COMPOSER_ROOT_VERSION=$COMPOSER_ROOT_VERSION >> $GITHUB_ENV + + echo "::group::composer update" + composer update --no-progress --ansi + echo "::endgroup::" + + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + + - name: Report the ICU version + run: uconv -V && php -i | grep 'ICU version' + + - name: Run intl-data tests + run: ./phpunit --group intl-data -v diff --git a/.github/workflows/phpunit-bridge.yml b/.github/workflows/phpunit-bridge.yml new file mode 100644 index 0000000000000..210029074f83e --- /dev/null +++ b/.github/workflows/phpunit-bridge.yml @@ -0,0 +1,38 @@ +name: PhpUnitBridge + +on: + push: + paths: + - 'src/Symfony/Bridge/PhpUnit/**' + pull_request: + paths: + - 'src/Symfony/Bridge/PhpUnit/**' + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + lint: + name: Lint + runs-on: Ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + php-version: "5.5" + + - name: Lint + run: find ./src/Symfony/Bridge/PhpUnit -name '*.php' | grep -v -e /Tests/ -e ForV6 -e ForV7 -e ForV8 -e ForV9 -e ConstraintLogicTrait | parallel -j 4 php -l {} diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml new file mode 100644 index 0000000000000..7b5fc5a4c8bbd --- /dev/null +++ b/.github/workflows/psalm.yml @@ -0,0 +1,61 @@ +name: Static analysis + +on: + pull_request: ~ + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + psalm: + name: Psalm + runs-on: Ubuntu-20.04 + + steps: + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + extensions: "json,memcached,mongodb,redis,xsl,ldap,dom" + ini-values: "memory_limit=-1" + coverage: none + + - name: Checkout target branch + uses: actions/checkout@v3 + with: + ref: ${{ github.base_ref }} + + - name: Checkout PR + uses: actions/checkout@v3 + + - name: Install dependencies + run: | + COMPOSER_HOME="$(composer config home)" + ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" + export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev + composer remove --dev --no-update --no-interaction symfony/phpunit-bridge + composer require --no-progress --ansi psalm/phar phpunit/phpunit:^9.5 php-http/discovery psr/event-dispatcher mongodb/mongodb + + - name: Generate Psalm baseline + run: | + git checkout composer.json + git checkout -m ${{ github.base_ref }} + cat .github/psalm/stubs/SetUpTearDownTrait.php > src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Bundle\\FrameworkBundle/' > src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Form/' > src/Symfony/Component/Form/Test/ForwardCompatTestTrait.php + cat .github/psalm/stubs/ForwardCompatTestTrait.php | sed 's/Component/Component\\Validator/' > src/Symfony/Component/Validator/Test/ForwardCompatTestTrait.php + + ./vendor/bin/psalm.phar --set-baseline=.github/psalm/psalm.baseline.xml --no-progress + git checkout -m FETCH_HEAD + + - name: Psalm + run: | + ./vendor/bin/psalm.phar --no-progress || ./vendor/bin/psalm.phar --output-format=github --no-progress diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000000..767141eab5bfe --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,228 @@ +name: PHPUnit + +on: + push: + pull_request: + +defaults: + run: + shell: bash + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + + tests: + name: Tests + + env: + extensions: amqp,apcu,igbinary,intl,mbstring,memcached,redis-5.3.4 + + strategy: + matrix: + include: + - php: '7.2' + - php: '7.4' + - php: '8.0' + mode: high-deps + - php: '8.1' + mode: low-deps + - php: '8.2' + #mode: experimental + fail-fast: false + + runs-on: ubuntu-20.04 + + steps: + - name: Checkout + uses: actions/checkout@v3 + with: + fetch-depth: 2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + ini-values: date.timezone=UTC,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=1,zend.assertions=1 + php-version: "${{ matrix.php }}" + extensions: "${{ env.extensions }}" + tools: flex + + - name: Configure environment + run: | + git config --global user.email "" + git config --global user.name "Symfony" + git config --global init.defaultBranch main + git config --global advice.detachedHead false + + COMPOSER_HOME="$(composer config home)" + ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" + + echo COLUMNS=120 >> $GITHUB_ENV + echo PHPUNIT="$(pwd)/phpunit --exclude-group tty,benchmark,intl-data,integration" >> $GITHUB_ENV + echo COMPOSER_UP='composer update --no-progress --ansi'$([[ "${{ matrix.php }}" = "8.2" ]] && echo ' --ignore-platform-req=php+') >> $GITHUB_ENV + + SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V) + SYMFONY_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | cut -d "'" -f2 | cut -d '.' -f 1-2) + SYMFONY_FEATURE_BRANCH=$(curl -s https://raw.githubusercontent.com/symfony/recipes/flex/main/index.json | jq -r '.versions."dev-name"') + + # Install the phpunit-bridge from a PR if required + # + # To run a PR with a patched phpunit-bridge, first submit the patch for the + # phpunit-bridge as a separate PR against the next feature-branch then + # uncomment and update the following line with that PR number + #SYMFONY_PHPUNIT_BRIDGE_PR=32886 + + if [[ $SYMFONY_PHPUNIT_BRIDGE_PR ]]; then + git fetch --depth=2 origin refs/pull/$SYMFONY_PHPUNIT_BRIDGE_PR/head + git rm -rq src/Symfony/Bridge/PhpUnit + git checkout -q FETCH_HEAD -- src/Symfony/Bridge/PhpUnit + SYMFONY_PHPUNIT_BRIDGE_REF=$(curl -s https://api.github.com/repos/symfony/symfony/pulls/$SYMFONY_PHPUNIT_BRIDGE_PR | jq -r .base.ref) + sed -i 's/"symfony\/phpunit-bridge": ".*"/"symfony\/phpunit-bridge": "'$SYMFONY_PHPUNIT_BRIDGE_REF'.x@dev"/' composer.json + rm -rf .phpunit + fi + + # Create local composer packages for each patched components and reference them in composer.json files when cross-testing components + if [[ ! "${{ matrix.mode }}" = *-deps ]]; then + php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit + else + echo SYMFONY_DEPRECATIONS_HELPER=weak >> $GITHUB_ENV + cp composer.json composer.json.orig + echo -e '{\n"require":{'"$(grep phpunit-bridge composer.json)"'"php":"*"},"minimum-stability":"dev"}' > composer.json + php .github/build-packages.php HEAD^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n') + mv composer.json composer.json.phpunit + mv composer.json.orig composer.json + fi + if [[ $SYMFONY_PHPUNIT_BRIDGE_PR ]]; then + git rm -fq -- src/Symfony/Bridge/PhpUnit/composer.json + git diff --staged -- src/Symfony/Bridge/PhpUnit/ | git apply -R --index + fi + + # For the highest branch, in high-deps mode, the version before it is checked out and tested with the locally patched components + if [[ "${{ matrix.mode }}" = high-deps && $SYMFONY_VERSION = $(echo "$SYMFONY_VERSIONS" | tail -n 1 | sed s/.//) ]]; then + echo FLIP='^' >> $GITHUB_ENV + SYMFONY_VERSION=$(echo "$SYMFONY_VERSIONS" | grep -FB1 /$SYMFONY_VERSION | head -n 1 | sed s/.//) + git fetch --depth=2 origin $SYMFONY_VERSION + git checkout -m FETCH_HEAD + echo COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h ') >> $GITHUB_ENV + fi + + # Skip the phpunit-bridge on bugfix-branches when not in *-deps mode + if [[ ! "${{ matrix.mode }}" = *-deps && $SYMFONY_VERSION != $SYMFONY_FEATURE_BRANCH ]]; then + echo COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' | xargs -I{} dirname {}) >> $GITHUB_ENV + else + echo COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist | xargs -I{} dirname {}) >> $GITHUB_ENV + fi + + # Legacy tests are skipped when deps=high and when the current branch version has not the same major version number as the next one + [[ "${{ matrix.mode }}" = high-deps && $SYMFONY_VERSION = *.4 ]] && echo LEGACY=,legacy >> $GITHUB_ENV || true + + echo SYMFONY_VERSION=$SYMFONY_VERSION >> $GITHUB_ENV + echo COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev >> $GITHUB_ENV + echo SYMFONY_REQUIRE=">=$([ '${{ matrix.mode }}' = low-deps ] && echo 3.4 || echo $SYMFONY_VERSION)" >> $GITHUB_ENV + [[ "${{ matrix.mode }}" = *-deps ]] && mv composer.json.phpunit composer.json || true + + - name: Install dependencies + run: | + echo "::group::composer update" + $COMPOSER_UP + echo "::endgroup::" + + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + + - name: Patch return types + if: "matrix.php == '8.1' && ! matrix.mode" + run: | + sed -i 's/"\*\*\/Tests\/"//' composer.json + composer install -q --optimize-autoloader + SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php + SYMFONY_PATCH_TYPE_DECLARATIONS=force=1 php .github/patch-types.php # ensure the script is idempotent + echo PHPUNIT="$PHPUNIT,legacy" >> $GITHUB_ENV + + - name: Run tests + run: | + _run_tests() { + local ok=0 + local title="$1$FLIP" + local start=$(date -u +%s) + OUTPUT=$(bash -xc "$2" 2>&1) || ok=1 + local end=$(date -u +%s) + + if [[ $ok -ne 0 ]]; then + printf "\n%-70s%10s\n" $title $(($end-$start))s + echo "$OUTPUT" + echo -e "\n::error::KO $title\\n" + else + printf "::group::%-68s%10s\n" $title $(($end-$start))s + echo "$OUTPUT" + echo -e "\n\\e[32mOK\\e[0m $title\\n\\n::endgroup::" + fi + + [[ "${{ matrix.mode }}" = experimental ]] || (exit $ok) + } + export -f _run_tests + + if [[ ! "${{ matrix.mode }}" = *-deps ]]; then + echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} '$PHPUNIT {}'" + + exit 0 + fi + + if [[ "${{ matrix.mode }}" = low-deps ]]; then + echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT'" + + exit 0 + fi + + (cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) + + # matrix.mode = high-deps + echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1 + + # get a list of the patched components (relies on .github/build-packages.php being called in the previous step) + (cd src/Symfony/Component/HttpFoundation; mv composer.bak composer.json) + PATCHED_COMPONENTS=$(git diff --name-only src/ | grep composer.json || true) + + # for 5.4 LTS, checkout and test previous major with the patched components (only for patched components) + if [[ $PATCHED_COMPONENTS && $SYMFONY_VERSION = 5.4 ]]; then + export FLIP='^' + SYMFONY_VERSION=$(echo $SYMFONY_VERSION | awk '{print $1 - 1}') + echo -e "\\n\\e[33;1mChecking out Symfony $SYMFONY_VERSION and running tests with patched components as deps\\e[0m" + export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev + export SYMFONY_REQUIRE=">=$SYMFONY_VERSION" + git fetch --depth=2 origin $SYMFONY_VERSION + git checkout -m FETCH_HEAD + PATCHED_COMPONENTS=$(echo "$PATCHED_COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort || true) + (cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb) + if [[ $PATCHED_COMPONENTS ]]; then + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + echo "$PATCHED_COMPONENTS" | parallel -j +3 "_run_tests {} 'cd {} && rm composer.lock vendor/ -Rf && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1 + fi + fi + + [[ ! $X ]] || (exit 1) + + - name: Run TTY tests + if: "! matrix.mode" + run: | + script -e -c './phpunit --group tty' /dev/null + + - name: Run tests with SIGCHLD enabled PHP + if: "matrix.php == '7.2' && ! matrix.mode" + run: | + mkdir build + cd build + wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/php-7.2.5-pcntl-sigchild.tar.bz2 + tar -xjf php-7.2.5-pcntl-sigchild.tar.bz2 + cd .. + + ./build/php/bin/php ./phpunit --colors=always src/Symfony/Component/Process diff --git a/.gitignore b/.gitignore index 0f504231b6e95..0c37517192aba 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,9 @@ vendor/ composer.lock phpunit.xml -.php_cs.cache +.php-cs-fixer.cache +.php-cs-fixer.php +.phpunit.result.cache composer.phar package.tar /packages.json diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 0000000000000..78ba7e3dbee08 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,64 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if (!file_exists(__DIR__.'/src')) { + exit(0); +} + +$fileHeaderComment = <<<'EOF' +This file is part of the Symfony package. + +(c) Fabien Potencier + +For the full copyright and license information, please view the LICENSE +file that was distributed with this source code. +EOF; + +return (new PhpCsFixer\Config()) + ->setRules([ + '@PHP71Migration' => true, + '@PHPUnit75Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'protected_to_private' => false, + 'native_constant_invocation' => ['strict' => false], + 'nullable_type_declaration_for_default_null_value' => ['use_nullable_type_declaration' => false], + 'header_comment' => ['header' => $fileHeaderComment], + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ->append([__FILE__]) + ->notPath('#/Fixtures/#') + ->exclude([ + // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code + // fixture templates + 'Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom', + // resource templates + 'Symfony/Bundle/FrameworkBundle/Resources/views/Form', + // explicit trigger_error tests + 'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/', + ]) + // Support for older PHPunit version + ->notPath('Symfony/Bridge/PhpUnit/SymfonyTestsListener.php') + ->notPath('#Symfony/Bridge/PhpUnit/.*Mock\.php#') + ->notPath('#Symfony/Bridge/PhpUnit/.*Legacy#') + // file content autogenerated by `var_export` + ->notPath('Symfony/Component/Translation/Tests/fixtures/resources.php') + // test template + ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') + // explicit trigger_error tests + ->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php') + ->notPath('Symfony/Component/ErrorHandler/Tests/DebugClassLoaderTest.php') + ) + ->setCacheFile('.php-cs-fixer.cache') +; diff --git a/.php_cs.dist b/.php_cs.dist deleted file mode 100644 index ac3e98528132c..0000000000000 --- a/.php_cs.dist +++ /dev/null @@ -1,57 +0,0 @@ -setRules([ - '@Symfony' => true, - '@Symfony:risky' => true, - '@PHPUnit48Migration:risky' => true, - 'php_unit_no_expectation_annotation' => false, // part of `PHPUnitXYMigration:risky` ruleset, to be enabled when PHPUnit 4.x support will be dropped, as we don't want to rewrite exceptions handling twice - 'array_syntax' => ['syntax' => 'short'], - 'fopen_flags' => false, - 'ordered_imports' => true, - 'protected_to_private' => false, - // Part of @Symfony:risky in PHP-CS-Fixer 2.13.0. To be removed from the config file once upgrading - 'native_function_invocation' => ['include' => ['@compiler_optimized'], 'scope' => 'namespaced', 'strict' => true], - // Part of future @Symfony ruleset in PHP-CS-Fixer To be removed from the config file once upgrading - 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], - ]) - ->setRiskyAllowed(true) - ->setFinder( - PhpCsFixer\Finder::create() - ->in(__DIR__.'/src') - ->append([__FILE__]) - ->exclude([ - 'Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures', - // directories containing files with content that is autogenerated by `var_export`, which breaks CS in output code - 'Symfony/Component/DependencyInjection/Tests/Fixtures', - 'Symfony/Component/Routing/Tests/Fixtures/dumper', - // fixture templates - 'Symfony/Component/Templating/Tests/Fixtures/templates', - 'Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TemplatePathsCache', - 'Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom', - // generated fixtures - 'Symfony/Component/VarDumper/Tests/Fixtures', - 'Symfony/Component/VarExporter/Tests/Fixtures', - // resource templates - 'Symfony/Bundle/FrameworkBundle/Resources/views/Form', - // explicit trigger_error tests - 'Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/', - ]) - // Support for older PHPunit version - ->notPath('Symfony/Bridge/PhpUnit/SymfonyTestsListener.php') - // file content autogenerated by `var_export` - ->notPath('Symfony/Component/Translation/Tests/fixtures/resources.php') - // test template - ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Custom/_name_entry_label.html.php') - // explicit heredoc test - ->notPath('Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Resources/views/translation.html.php') - // explicit trigger_error tests - ->notPath('Symfony/Component/Debug/Tests/DebugClassLoaderTest.php') - // invalid annotations on purpose - ->notPath('Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php') - ) -; diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 353a51e18077d..0000000000000 --- a/.travis.yml +++ /dev/null @@ -1,264 +0,0 @@ -language: php - -dist: xenial - -git: - depth: 2 - -addons: - apt_packages: - - parallel - - language-pack-fr-base - - ldap-utils - - slapd - - zookeeperd - - libzookeeper-mt-dev - - rabbitmq-server - -env: - global: - - MIN_PHP=7.1.3 - - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/shims/php - - MESSENGER_AMQP_DSN=amqp://localhost/%2f/messages - - MESSENGER_REDIS_DSN=redis://127.0.0.1:7001/messages - -matrix: - include: - - php: 7.1 - - php: 7.2 - env: deps=high - - php: 7.3 - env: deps=low - - fast_finish: true - -cache: - directories: - - .phpunit - - php-$MIN_PHP - - ~/php-ext - -services: - - memcached - - mongodb - - redis-server - - rabbitmq - - docker - -before_install: - - | - # Enable Sury ppa - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 - sudo add-apt-repository -y ppa:ondrej/php - sudo rm /etc/apt/sources.list.d/google-chrome.list - sudo rm /etc/apt/sources.list.d/mongodb-3.4.list - sudo apt update - sudo apt install -y librabbitmq-dev libsodium-dev - - - | - # Start Redis cluster - docker pull grokzen/redis-cluster:5.0.4 - docker run -d -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 --name redis-cluster grokzen/redis 8000 -cluster:5.0.4 - export REDIS_CLUSTER_HOSTS='localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' - - - | - # General configuration - set -e - stty cols 120 - mkdir /tmp/slapd - if [ ! -e /tmp/slapd-modules ]; then - [ -d /usr/lib/openldap ] && ln -s /usr/lib/openldap /tmp/slapd-modules || ln -s /usr/lib/ldap /tmp/slapd-modules - fi - slapd -f src/Symfony/Component/Ldap/Tests/Fixtures/conf/slapd.conf -h ldap://localhost:3389 & - [ -d ~/.composer ] || mkdir ~/.composer - cp .composer/* ~/.composer/ - export PHPUNIT=$(readlink -f ./phpunit) - export PHPUNIT_X="$PHPUNIT --exclude-group tty,benchmark,intl-data" - export COMPOSER_UP='composer update --no-progress --no-suggest --ansi' - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n') - find ~/.phpenv -name xdebug.ini -delete - - nanoseconds () { - local cmd="date" - local format="+%s%N" - local os=$(uname) - if hash gdate > /dev/null 2>&1; then - cmd="gdate" - elif [[ "$os" = Darwin ]]; then - format="+%s000000000" - fi - $cmd -u $format - } - export -f nanoseconds - - # tfold is a helper to create folded reports - tfold () { - local title="🐘 $PHP $1" - local fold=$(echo $title | sed -r 's/[^-_A-Za-z0-9]+/./g') - shift - local id=$(printf %08x $(( RANDOM * RANDOM ))) - local start=$(nanoseconds) - echo -e "travis_fold:start:$fold" - echo -e "travis_time:start:$id" - echo -e "\\e[1;34m$title\\e[0m" - - bash -xc "$*" 2>&1 - local ok=$? - local end=$(nanoseconds) - echo -e "\\ntravis_time:end:$id:start=$start,finish=$end,duration=$(($end-$start))" - (exit $ok) && - echo -e "\\e[32mOK\\e[0m $title\\n\\ntravis_fold:end:$fold" || - echo -e "\\e[41mKO\\e[0m $title\\n" - (exit $ok) - } - export -f tfold - - # tpecl is a helper to compile and cache php extensions - tpecl () { - local ext_name=$1 - local ext_so=$2 - local INI=$3 - local input=${4:-yes} - local ext_dir=$(php -r "echo ini_get('extension_dir');") - local ext_cache=~/php-ext/$(basename $ext_dir)/$ext_name - - if [[ -e $ext_cache/$ext_so ]]; then - echo extension = $ext_cache/$ext_so >> $INI - else - rm ~/.pearrc /tmp/pear 2>/dev/null || true - mkdir -p $ext_cache - echo $input | pecl install -f $ext_name && - cp $ext_dir/$ext_so $ext_cache - fi - } - export -f tpecl - - - | - # Install sigchild-enabled PHP to test the Process component on the lowest PHP matrix line - if [[ ! $deps && $TRAVIS_PHP_VERSION = ${MIN_PHP%.*} && ! -d php-$MIN_PHP/sapi ]]; then - wget http://php.net/get/php-$MIN_PHP.tar.bz2/from/this/mirror -O - | tar -xj && - (cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2) - fi - - - | - # php.ini configuration - for PHP in $TRAVIS_PHP_VERSION $php_extra; do - phpenv global $PHP 2>/dev/null || (cd / && wget https://s3.amazonaws.com/travis-php-archives/binaries/ubuntu/14.04/x86_64/php-$PHP.tar.bz2 -O - | tar -xj) - INI=~/.phpenv/versions/$PHP/etc/conf.d/travis.ini - echo date.timezone = Europe/Paris >> $INI - echo memory_limit = -1 >> $INI - echo session.gc_probability = 0 >> $INI - echo opcache.enable_cli = 1 >> $INI - echo apc.enable_cli = 1 >> $INI - echo extension = memcached.so >> $INI - done - - - | - # Install extra PHP extensions - for PHP in $TRAVIS_PHP_VERSION $php_extra; do - export PHP=$PHP - phpenv global $PHP - INI=~/.phpenv/versions/$PHP/etc/conf.d/travis.ini - if ! php --ri sodium > /dev/null; then - tfold ext.libsodium tpecl libsodium sodium.so $INI - fi - - tfold ext.apcu tpecl apcu-5.1.16 apcu.so $INI - tfold ext.mongodb tpecl mongodb-1.6.0alpha1 mongodb.so $INI - tfold ext.igbinary tpecl igbinary-2.0.8 igbinary.so $INI - tfold ext.zookeeper tpecl zookeeper-0.7.1 zookeeper.so $INI - tfold ext.amqp tpecl amqp-1.9.4 amqp.so $INI - tfold ext.redis tpecl redis-4.3.0 redis.so $INI "no" - done - - | - # List all php extensions with versions - - php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' - - - | - # Load fixtures - if [[ ! $skip ]]; then - ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/base.ldif && - ldapadd -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony -f src/Symfony/Component/Ldap/Tests/Fixtures/data/fixtures.ldif - fi - -install: - - | - # Create local composer packages for each patched components and reference them in composer.json files when cross-testing components - if [[ ! $deps ]]; then - php .github/build-packages.php HEAD^ src/Symfony/Bridge/PhpUnit src/Symfony/Contracts - else - export SYMFONY_DEPRECATIONS_HELPER=weak && - cp composer.json composer.json.orig && - echo -e '{\n"require":{'"$(grep phpunit-bridge composer.json)"'"php":"*"},"minimum-stability":"dev"}' > composer.json && - php .github/build-packages.php HEAD^ $COMPONENTS && - mv composer.json composer.json.phpunit && - mv composer.json.orig composer.json - fi - - - | - # For the master branch, when deps=high, the version before master is checked out and tested with the locally patched components - if [[ $deps = high && $TRAVIS_BRANCH = master ]]; then - SYMFONY_VERSION=$(git ls-remote --heads | grep -o '/[1-9].*' | tail -n 1 | sed s/.//) && - git fetch origin $SYMFONY_VERSION && - git checkout -m FETCH_HEAD && - COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n') - else - SYMFONY_VERSION=$(cat composer.json | grep '^ *"dev-master". *"[1-9]' | grep -o '[0-9.]*') - fi - - - | - # Skip the phpunit-bridge on not-master branches when $deps is empty - if [[ ! $deps && $TRAVIS_BRANCH != master ]]; then - COMPONENTS=$(find src/Symfony -mindepth 3 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' -printf '%h\n') - fi - - - | - # Install symfony/flex - if [[ $deps = low ]]; then - export SYMFONY_REQUIRE='>=2.3' - else - export SYMFONY_REQUIRE=">=$SYMFONY_VERSION" - fi - composer global require --no-progress --no-scripts --no-plugins symfony/flex dev-master - - - | - # Legacy tests are skipped when deps=high and when the current branch version has not the same major version number than the next one - [[ $deps = high && ${SYMFONY_VERSION%.*} != $(git show $(git ls-remote --heads | grep -FA1 /$SYMFONY_VERSION | tail -n 1):composer.json | grep '^ *"dev-master". *"[1-9]' | grep -o '[0-9]*' | head -n 1) ]] && LEGACY=,legacy - - export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev - if [[ $deps ]]; then mv composer.json.phpunit composer.json; fi - - - php -i - - - | - run_tests () { - set -e - export PHP=$1 - if [[ $PHP != $TRAVIS_PHP_VERSION && $TRAVIS_PULL_REQUEST != false ]]; then - echo -e "\\n\\e[1;34mIntermediate PHP version $PHP is skipped for pull requests.\\e[0m" - break - fi - phpenv global $PHP - ([[ $deps ]] && cd src/Symfony/Component/HttpFoundation; composer config platform.ext-mongodb 1.6.0; composer require --dev --no-update mongodb/mongodb) - tfold 'composer update' $COMPOSER_UP - tfold 'phpunit install' ./phpunit install - if [[ $deps = high ]]; then - echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && $COMPOSER_UP && $PHPUNIT_X$LEGACY'" - elif [[ $deps = low ]]; then - [[ -e ~/php-ext/composer-lowest.lock.tar ]] && tar -xf ~/php-ext/composer-lowest.lock.tar - tar -cf ~/php-ext/composer-lowest.lock.tar --files-from /dev/null - php .github/rm-invalid-lowest-lock-files.php $COMPONENTS - echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && ([ -e composer.lock ] && ${COMPOSER_UP/update/install} || $COMPOSER_UP --prefer-lowest --prefer-stable) && $PHPUNIT_X'" - echo "$COMPONENTS" | xargs -n1 -I{} tar --append -f ~/php-ext/composer-lowest.lock.tar {}/composer.lock - else - echo "$COMPONENTS" | parallel --gnu "tfold {} $PHPUNIT_X {}" - tfold src/Symfony/Component/Console.tty $PHPUNIT src/Symfony/Component/Console --group tty - if [[ $PHP = ${MIN_PHP%.*} ]]; then - export PHP=$MIN_PHP - tfold src/Symfony/Component/Process.sigchild SYMFONY_DEPRECATIONS_HELPER=weak php-$MIN_PHP/sapi/cli/php ./phpunit --colors=always src/Symfony/Component/Process/ - fi - fi - } - -script: - - for PHP in $TRAVIS_PHP_VERSION $php_extra; do (run_tests $PHP); done diff --git a/CHANGELOG-4.3.md b/CHANGELOG-4.3.md index 965e52e7db2a5..9c95e7e327a31 100644 --- a/CHANGELOG-4.3.md +++ b/CHANGELOG-4.3.md @@ -7,6 +7,402 @@ in 4.3 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.3.0...v4.3.1 +* 4.3.10 (2020-01-21) + + * bug #35364 [Yaml] Throw on unquoted exclamation mark (fancyweb) + * bug #35065 [Security] Use supportsClass in addition to UnsupportedUserException (linaori) + * bug #35343 [Security] Fix RememberMe with null password (jderusse) + * bug #34223 [DI] Suggest typed argument when binding fails with untyped argument (gudfar) + * bug #35324 [HttpClient] Fix strict parsing of response status codes (Armando-Walmeric) + * bug #35318 [Yaml] fix PHP const mapping keys using the inline notation (xabbuh) + * bug #35304 [HttpKernel] Fix that no-cache MUST revalidate with the origin (mpdude) + * bug #35299 Avoid `stale-if-error` in FrameworkBundle's HttpCache if kernel.debug = true (mpdude) + * bug #35151 [DI] deferred exceptions in ResolveParameterPlaceHoldersPass (Islam93) + * bug #35278 [EventDispatcher]Β expand listener in place (xabbuh) + * bug #35254 [PHPUnit-Bridge] Fail-fast in simple-phpunit if one of the passthru() commands fails (mpdude) + * bug #35261 [Routing] Fix using a custom matcher & generator dumper class (fancyweb) + * bug #34643 [Dotenv] Fixed infinite loop with missing quote followed by quoted value (naitsirch) + * bug #35239 [Security\Http] Prevent canceled remember-me cookie from being accepted (chalasr) + * bug #35267 [Debug] fix ClassNotFoundFatalErrorHandler (nicolas-grekas) + * bug #35193 [TwigBridge] button_widget now has its title attr translated even if its label = null or false (stephen-lewis) + * bug #35219 [PhpUnitBridge] When using phpenv + phpenv-composer plugin, composer executable is wrapped into a bash script (oleg-andreyev) + * bug #35150 [Messenger] Added check if json_encode succeeded (toooni) + * bug #35170 [FrameworkBundle][TranslationUpdateCommand] Do not output positive feedback on stderr (fancyweb) + * bug #35223 [HttpClient] Don't read from the network faster than the CPU can deal with (nicolas-grekas) + * bug #35214 [DI] DecoratorServicePass should keep container.service_locator on the decorated definition (malarzm) + * bug #35210 [HttpClient] NativeHttpClient should not send >1.1 protocol version (nicolas-grekas) + * bug #33672 [Mailer] Remove line breaks in email attachment content (Stuart Fyfe) + * bug #35101 [Routing] Fix i18n routing when the url contains the locale (fancyweb) + * bug #35124 [TwigBridge][Form] Added missing help messages in form themes (cmen) + * bug #35168 [HttpClient] fix capturing SSL certificates with NativeHttpClient (nicolas-grekas) + * bug #35134 [PropertyInfo] Fix BC issue in phpDoc Reflection library (jaapio) + * bug #35173 [Mailer][MailchimpBridge] Fix missing attachments when sending via Mandrill API (vilius-g) + * bug #35172 [Mailer][MailchimpBridge] Fix incorrect sender address when sender has name (vilius-g) + * bug #35125 [Translator] fix performance issue in MessageCatalogue and catalogue operations (ArtemBrovko) + * bug #35120 [HttpClient] fix scheduling pending NativeResponse (nicolas-grekas) + * bug #35117 [Cache] do not overwrite variable value (xabbuh) + * bug #35113 [VarDumper] Fix "Undefined index: argv" when using CliContextProvider (xepozz) + * bug #35103 [Translation] Use `locale_parse` for computing fallback locales (alanpoulain) + * bug #35094 [Console] Fix filtering out identical alternatives when there is a command loader (fancyweb) + * bug #35039 [DI] skip looking for config class when the extension class is anonymous (nicolas-grekas) + * bug #35049 [ProxyManager] fix generating proxies for root-namespaced classes (nicolas-grekas) + * bug #35022 [Dotenv] FIX missing getenv (mccullagh) + * bug #35025 [HttpClient][Psr18Client] Remove Psr18ExceptionTrait (fancyweb) + * bug #35014 [HttpClient] make pushed responses retry-able (nicolas-grekas) + * bug #35010 [VarDumper] ignore failing __debugInfo() (nicolas-grekas) + * bug #34998 [DI] fix auto-binding service providers to their service subscribers (nicolas-grekas) + * bug #33670 [DI] Service locators can't be decorated (malarzm) + * bug #35000 [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt (fancyweb) + * bug #34996 Fix displaying anonymous classes on PHP 7.4 (nicolas-grekas) + * bug #29839 [Validator] fix comparisons with null values at property paths (xabbuh) + * bug #34900 [DoctrineBridge] Fixed submitting invalid ids when using queries with limit (HeahDude) + * bug #34791 [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer (vudaltsov) + * bug #34956 [Messenger][AMQP] Use delivery_mode=2 by default (lyrixx) + * bug #34915 [FrameworkBundle] Fix invalid Windows path normalization in TemplateNameParser (mvorisek) + * bug #34981 stop using deprecated Doctrine persistence classes (xabbuh) + * bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb) + * bug #34955 Require doctrine/persistence ^1.3 (nicolas-grekas) + * bug #34923 [DI] Fix support for immutable setters in CallTrait (Lctrs) + * bug #34918 [Translation] fix memoryleak in PhpFileLoader (nicolas-grekas) + * bug #34920 [Routing] fix memoryleak when loading compiled routes (nicolas-grekas) + * bug #34787 [Cache] Propagate expiry when syncing items in ChainAdapter (trvrnrth) + * bug #34896 [Cache] fix memory leak when using PhpFilesAdapter (nicolas-grekas) + * bug #34438 [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given (mpdude) + * bug #34449 [Yaml] Implement multiline string as scalar block for tagged values (natepage) + * bug #34601 [MonologBridge] Fix debug processor datetime type (mRoca) + * bug #34842 [ExpressionLanguage] Process division by zero (tigr1991) + * bug #34902 [PropertyAccess] forward caught exception (xabbuh) + * bug #34888 [TwigBundle] add tags before processing them (xabbuh) + * bug #34762 [Config] never try loading failed classes twice with ClassExistenceResource (nicolas-grekas) + * bug #34839 [Cache] fix memory leak when using PhpArrayAdapter (nicolas-grekas) + * bug #34812 [Yaml] fix parsing negative octal numbers (xabbuh) + * bug #34854 [Messenger]Β gracefully handle missing event dispatchers (xabbuh) + * bug #34788 [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass (fancyweb) + * bug #34755 [FrameworkBundle] resolve service locators in `debug:*` commands (nicolas-grekas) + * bug #34832 [Validator] Allow underscore character "_" in URL username and password (romainneutron) + * bug #34776 [DI] fix resolving bindings for named TypedReference (nicolas-grekas) + * bug #34738 [SecurityBundle] Passwords are not encoded when algorithm set to "true" (nieuwenhuisen) + * bug #34779 [Security] do not validate passwords when the hash is null (xabbuh) + * bug #34757 [DI] Fix making the container path-independent when the app is in /app (nicolas-grekas) + +* 4.3.9 (2019-12-01) + + * bug #34649 more robust initialization from request (dbu) + * bug #34671 [Security] Fix clearing remember-me cookie after deauthentication (chalasr) + * bug #34711 Fix the translation commands when a template contains a syntax error (fabpot) + * bug #34560 [Config][ReflectionClassResource] Handle parameters with undefined constant as their default values (fancyweb) + * bug #34695 [Config] don't break on virtual stack frames in ClassExistenceResource (nicolas-grekas) + * bug #34716 [DependencyInjection] fix dumping number-like string parameters (xabbuh) + * bug #34558 [Console] Fix autocomplete multibyte input support (fancyweb) + * bug #34130 [Console] Fix commands description with numeric namespaces (fancyweb) + * bug #34677 [EventDispatcher] Better error reporting when arguments to dispatch() are swapped (rimas-kudelis) + * bug #33573 [TwigBridge] Add row_attr to all form themes (fancyweb) + * bug #34019 [Serializer] CsvEncoder::NO_HEADERS_KEY ignored when used in constructor (Dario Savella) + * bug #34083 [Form] Keep preferred_choices order for choice groups (vilius-g) + * bug #34091 [Debug] work around failing chdir() on Darwin (mary2501) + * bug #34305 [PhpUnitBridge] Read configuration CLI directive (ro0NL) + * bug #34490 [Serializer] Fix MetadataAwareNameConverter usage with string group (antograssiot) + * bug #34632 [Console] Fix trying to access array offset on value of type int (Tavafi) + * bug #34669 [HttpClient] turn exception into log when the request has no content-type (nicolas-grekas) + * bug #34636 [VarDumper] notice on potential undefined index (sylvainmetayer) + * bug #34668 [Cache] Make sure we get the correct number of values from redis::mget() (thePanz) + * bug #34569 [Workflow] Apply the same logic of precedence between the apply() and the buildTransitionBlockerList() method (lyrixx) + * bug #34533 [Monolog Bridge] Fixed accessing static property as non static. (Sander-Toonen) + * bug #34546 [Serializer] Add DateTimeZoneNormalizer into Dependency Injection (jewome62) + * bug #34547 [Messenger] Error when specified default bus is not among the configured (vudaltsov) + * bug #34551 [Security] SwitchUser is broken when the User Provider always returns a valid user (tucksaun) + * bug #34385 Avoid empty "If-Modified-Since" header in validation request (mpdude) + * bug #34458 [Validator] ConstraintValidatorTestCase: add missing return value to mocked validate method calls (ogizanagi) + * bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim) + * bug #34448 [Form] allow button names to start with uppercase letter (xabbuh) + * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) + * bug #34366 [HttpFoundation] Allow redirecting to URLs that contain a semicolon (JayBizzle) + * bug #34397 [FrameworkBundle] Remove project dir from Translator cache vary scanned directories (fancyweb) + * bug #34408 [Cache] catch exceptions when using PDO directly (xabbuh) + * bug #34410 [HttpFoundation] Fix MySQL column type definition. (jbroutier) + * bug #34398 [Config] fix id-generation for GlobResource (nicolas-grekas) + * bug #34396 [Finder] Allow ssh2 stream wrapper for sftp (damienalexandre) + * bug #34383 [DI] Use reproducible entropy to generate env placeholders (nicolas-grekas) + * bug #34381 [WebProfilerBundle] Require symfony/twig-bundle (fancyweb) + +* 4.3.8 (2019-11-13) + + * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) + * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) + * security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) + * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) + * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) + +* 4.3.7 (2019-11-11) + + * bug #34294 [Workflow] Fix error when we use ValueObject for the marking property (FabienSalles) + * bug #34297 [DI] fix locators with numeric keys (nicolas-grekas) + * bug #34282 [DI] Dont cache classes with missing parents (nicolas-grekas) + * bug #34287 [HttpClient] Fix a crash when calling CurlHttpClient::__destruct() (dunglas) + * bug #34129 [FrameworkBundle][Translation] Invalidate cached catalogues when the scanned directories change (fancyweb) + * bug #34246 [Serializer] Use context to compute MetadataAwareNameConverter cache (antograssiot) + * bug #34251 [HttpClient] expose only gzip when doing transparent compression (nicolas-grekas) + * bug #34244 [Inflector] add support for 'species' (jeffreymoelands) + * bug #34085 [Console] Detect dimensions using mode CON if vt100 is supported (rtek) + * bug #34199 [HttpClient] Retry safe requests using HTTP/1.1 when HTTP/2 fails (nicolas-grekas) + * bug #34192 [Routing] Fix URL generator instantiation (X-Coder264, HypeMC) + * bug #34134 [Messenger] fix retry of messages losing the routing key and properties (Tobion) + * bug #34181 [Stopwatch] Fixed bug in getDuration when counting multiple ongoing periods (TimoBakx) + * bug #34165 [PropertyInfo] Fixed type extraction for nullable collections of non-nullable elements (happyproff) + * bug #34179 [Stopwatch] Fixed a bug in StopwatchEvent::getStartTime (TimoBakx) + * bug #34203 [FrameworkBundle] [HttpKernel] fixed correct EOL and EOM month (erics86) + * bug #34035 [Serializer] Fix property name usage for denormalization (antograssiot) + +* 4.3.6 (2019-11-01) + + * bug #34198 [HttpClient] Fix perf issue when doing thousands of requests with curl (nicolas-grekas) + * bug #33998 [Config] Disable default alphabet sorting in glob function due of unstable sort (hurricane-voronin) + * bug #34144 [Serializer] Improve messages for unexpected resources values (fancyweb) + * bug #34186 [HttpClient] always return the empty string when the response cannot have a body (nicolas-grekas) + * bug #34167 [HttpFoundation] Allow to not pass a parameter to Request::isMethodSafe() (dunglas) + * bug #33828 [DoctrineBridge] Auto-validation must work if no regex are passed (dunglas) + * bug #34080 [SecurityBundle] correct types for default arguments for firewall configs (shieldo) + * bug #34152 [Workflow] Made the configuration more robust for the 'property' key (lyrixx) + * bug #34154 [HttpClient] fix handling of 3xx with no Location header - ignore Content-Length when no body is expected (nicolas-grekas) + * bug #34140 [Security/Core] make NativePasswordEncoder use sodium to validate passwords when possible (nicolas-grekas) + * bug #33999 [Form] Make sure to collect child forms created on *_SET_DATA events (yceruto) + * bug #34090 [WebProfilerBundle] Improve display in Email panel for dark theme (antograssiot) + * bug #34116 [HttpClient] ignore the body of responses to HEAD requests (nicolas-grekas) + * bug #32456 [Messenger] use database platform to convert correctly the DateTime (roukmoute) + * bug #34107 [Messenger] prevent infinite redelivery loops and blocked queues (Tobion) + * bug #32341 [Messenger] Show exceptions after multiple retries (TimoBakx) + * bug #34082 Revert "[Messenger] Fix exception message of failed message is dropped (Tobion) + * bug #34021 [TwigBridge] do not render errors for checkboxes twice (xabbuh) + * bug #34017 [Messenger] Fix ignored options in redis transport (chalasr) + * bug #34041 [HttpKernel] fix wrong removal of the just generated container dir (nicolas-grekas) + * bug #34024 [Routing] fix route loading with wildcard, but dir or file is empty (gseidel) + * bug #34023 [Dotenv] allow LF in single-quoted strings (nicolas-grekas) + * bug #33818 [Yaml] Throw exception for tagged invalid inline elements (gharlan) + * bug #33994 [Mailer] Fix Mandrill Transport API payload for named addresses (MichaΓ«l Perrin) + * bug #33985 [HttpClient] workaround curl_multi_select() issue (nicolas-grekas) + * bug #33948 [PropertyInfo] Respect property name case when guessing from public method name (antograssiot) + * bug #33962 [Cache] fixed TagAwareAdapter returning invalid cache (v-m-i) + * bug #33958 [DI] Add extra type check to php dumper (gquemener) + * bug #33965 [HttpFoundation] Add plus character `+` to legal mime subtype (ilzrv) + * bug #32943 [Dotenv] search variable values in ENV first then env file (soufianZantar) + * bug #33943 [VarDumper] fix resetting the "bold" state in CliDumper (nicolas-grekas) + * bug #33936 [HttpClient] Missing argument in method_exists (detinkin) + * bug #33937 [Cache] ignore unserialization failures in AbstractTagAwareAdapter::doDelete() (nicolas-grekas) + * bug #33935 [HttpClient] send `Accept: */*` by default, fix removing it when needed (nicolas-grekas) + * bug #33922 [Cache] remove implicit dependency on symfony/filesystem (nicolas-grekas) + * bug #33927 Allow to set SameSite config to 'none' (ihmels) + * bug #33930 [Cache] clean tags folder on invalidation (nicolas-grekas) + * bug #33919 [VarDumper] fix array key error for class SymfonyCaster (zcodes) + * bug #33885 [Form][DateTimeImmutableToDateTimeTransformer] Preserve microseconds and use \DateTime::createFromImmutable() when available (fancyweb) + * bug #33900 [HttpKernel] Fix to populate $dotenvVars in data collector when not using putenv() (mynameisbogdan) + +* 4.3.5 (2019-10-07) + + * bug #33742 [Crawler] document $default as string|null (nicolas-grekas) + * bug #32308 [Messenger] DoctrineTransport: ensure auto setup is only done once (bendavies) + * bug #33871 [HttpClient] bugfix exploding values of headers (michaljusiega) + * bug #33834 [Validator] Fix ValidValidator group cascading usage (fancyweb) + * bug #33863 [Routing] gracefully handle docref_root ini setting (nicolas-grekas) + * bug #33846 [Cache] give 100ms before starting the expiration countdown (nicolas-grekas) + * bug #33853 [HttpClient] fix "no_proxy" option ignored in NativeHttpClient (Harry-Dunne) + * bug #33841 [VarDumper] fix dumping uninitialized SplFileInfo (nicolas-grekas) + * bug #33842 [Cache] fix logger usage in CacheTrait::doGet() (nicolas-grekas) + * bug #33835 [Workflow] Fixed BC break on WorkflowInterface (lyrixx) + * bug #33799 [Security]: Don't let falsy usernames slip through impersonation (j4nr6n) + * bug #33814 [HttpFoundation] Check if data passed to SessionBagProxy::initialize is an array (mynameisbogdan) + * bug #33744 [DI] Add CSV env var processor tests / support PHP 7.4 (ro0NL) + * bug #33805 [FrameworkBundle] Fix wrong returned status code in ConfigDebugCommand (jschaedl) + * bug #33781 [AnnotationCacheWarmer] add RedirectController to annotation cache (jenschude) + * bug #33777 Fix the :only-of-type pseudo class selector (jakzal) + * bug #32051 [Serializer] Add CsvEncoder tests for PHP 7.4 (ro0NL) + * feature #33776 Copy phpunit.xsd to a predictable path (julienfalque) + * bug #33759 [Security/Http] fix parsing X509 emailAddress (nicolas-grekas) + * bug #33733 [Serializer] fix denormalization of string-arrays with only one element (mkrauser) + * bug #33754 [Cache] fix known tag versions ttl check (SwenVanZanten) + * bug #33646 [HttpFoundation] allow additinal characters in not raw cookies (marie) + * bug #33748 [Console] Do not include hidden commands in suggested alternatives (m-vo) + * bug #33625 [DependencyInjection] Fix wrong exception when service is synthetic (k0d3r1s) + * bug #32979 [Messenger] return empty envelopes when RetryableException occurs (surikman) + * bug #32522 [Validator] Accept underscores in the URL validator, as the URL will load (battye) + * bug #32437 Fix toolbar load when GET params are present in "_wdt" route (Molkobain) + * bug #32925 [Translation] Collect original locale in case of fallback translation (digilist) + * bug #33691 [HttpClient] fix race condition when reading response with informational status (nicolas-grekas) + * bug #33727 [HttpClient] workaround bad Content-Length sent by old libcurl (nicolas-grekas) + * bug #31198 [FrameworkBundle] Fix framework bundle lock configuration not working as expected (HypeMC) + * bug #33719 [Cache] dont override native Memcached options (nicolas-grekas) + * bug #33703 [Cache] fail gracefully when locking is not supported (nicolas-grekas) + * bug #33713 Fix exceptions (PDOException) error code type (fruty) + * bug #32335 [Form] Names for buttons should start with lowercase (mcfedr) + * bug #33706 [Mailer][Messenger] ensure legacy event dispatcher compatibility (xabbuh) + * bug #33688 Add missing row_attr option to FormType (mcsky) + * bug #33693 [Security] use LegacyEventDispatcherProxy (dmaicher) + * bug #33675 [PhpUnit] Fix usleep mock return value (fabpot) + * bug #33652 [Cache] skip igbinary on PHP 7.4.0 (nicolas-grekas) + * bug #33643 [HttpClient] fix throwing HTTP exceptions when the 1st chunk is emitted (nicolas-grekas) + * bug #33618 fix tests depending on other components' tests (xabbuh) + * bug #33626 [PropertyInfo]Β ensure compatibility with type resolver 0.5 (xabbuh) + * bug #33620 [Twig] Fix Twig config extra keys (fabpot) + * bug #33600 [Messenger] Fix exception message of failed message is dropped on retry (tienvx) + * bug #33601 [HttpClient] Add default value for Accept header (numerogeek) + * bug #33340 [Finder] Adjust regex to correctly match comments in gitignore contents (Jeroeny) + * bug #33588 [PropertyInfo] ensure compatibility with type resolver 0.5 (xabbuh) + * bug #33575 [WebProfilerBundle] Fix time panel legend buttons (fancyweb) + * bug #33571 [Inflector] add support 'see' to 'ee' for singularize 'fees' to 'fee' (maxhelias) + * bug #32763 [Console] Get dimensions from stty on windows if possible (rtek) + * bug #33570 Fixed cache pools affecting each other due to an overwritten seed variable (roed) + * bug #33517 [Yaml] properly catch legacy tag syntax usages (xabbuh) + * bug #33546 [DependencyInjection] Accept existing interfaces as valid named args (fancyweb) + * bug #33547 [HttpClient] Re-enable Server Push support (dunglas) + * bug #33521 Fixed incompatibility between ServiceSubscriberTrait and classes with protected $container property (a-menshchikov) + * bug #33518 [Yaml] don't dump a scalar tag value on its own line (xabbuh) + * bug #33505 [HttpClient] fallbackto CURLMOPT_MAXCONNECTS when CURLMOPT_MAX_HOST_CONNECTIONS is not available (nicolas-grekas) + * bug #32818 [HttpKernel] Fix getFileLinkFormat() to avoid returning the wrong URL in Profiler (Arman-Hosseini) + * bug #33487 [HttpKernel] Fix Apache mod_expires Session Cache-Control issue (pbowyer) + * bug #33469 [FrameworkBundle] Fixed suggested package for missing server:dump command (lyrixx) + * bug #31964 [Router] routing cache crash when using generator_class (dFayet) + * bug #33481 [Messenger] fix empty amqp body returned as false (Tobion) + * bug #33387 [Mailer] maintain sender/recipient name in SMTP envelopes (xabbuh) + * bug #33449 Fix gmail relay (Beno!t POLASZEK) + * bug #33391 [HttpClient] fix support for 103 Early Hints and other informational status codes (nicolas-grekas) + * bug #33444 [HttpClient] improve handling of HTTP/2 PUSH, disable it by default (nicolas-grekas) + * bug #33435 [Validator] Only handle numeric values in DivisibleBy (fancyweb) + * bug #33437 Fix #33427 (sylfabre) + * bug #33439 [Validator] Sync string to date behavior and throw a better exception (fancyweb) + * bug #33436 [DI] fix support for "!tagged_locator foo" (nicolas-grekas) + * bug #32903 [PHPUnit Bridge] Avoid registering listener twice (alexpott) + * bug #33432 [Mailer] Fix Mailgun support when a response is not JSON as expected (fabpot) + * bug #33402 [Finder] Prevent unintentional file locks in Windows (jspringe) + * bug #33376 [Mailer] Remove the default dispatcher in AbstractTransport (fabpot) + * bug #33357 [FrameworkBundle] Fix about command not showing .env vars (brentybh) + * bug #33396 Fix #33395 PHP 5.3 compatibility (kylekatarnls) + * bug #33363 [Routing] fix 8000 static route reordering when a previous dynamic route conflicts (nicolas-grekas) + * bug #33385 [Console] allow Command::getName() to return null (nicolas-grekas) + * bug #33353 Return null as Expire header if it was set to null (danrot) + * bug #33382 [ProxyManager] remove ProxiedMethodReturnExpression polyfill (nicolas-grekas) + * bug #33377 [Yaml] fix dumping not inlined scalar tag values (xabbuh) + +* 4.3.4 (2019-08-26) + + * bug #33335 [DependencyInjection] Fixed the `getServiceIds` implementation to always return aliases (pdommelen) + * bug #33298 [Messenger] Stop worker when it should stop (tienvx) + * bug #33292 [VarExporter] fix support for PHP 7.4 (nicolas-grekas) + * bug #33282 [HttpKernel] Do not extend the new SF 4.3 ControllerEvent so we can make it final (Tobion) + * bug #33278 [FrameworkBundle] Fix BrowserKit assertions to make them compatible with Panther (dunglas) + * bug #33216 [Mime] Trim and remove line breaks from NamedAddress name arg (maldoinc) + * bug #33124 [Config] Add handling for ignored keys in ArrayNode::mergeValues. (Alexandre Parent) + * bug #33244 [Router] Fix TraceableUrlMatcher behaviour with trailing slash (Xavier Leune) + * bug #33232 Fix handling for session parameters (vkhramtsov) + * bug #32497 [Messenger] DispatchAfterCurrentBusMiddleware does not cancel messages from delayed handlers (Nyholm, BastienClement) + * bug #33127 [Messenger] make delay exchange and queues durable like the normal ones by default (Tobion) + * bug #33210 [Mailer] Don't duplicate addresses in Sendgrid Transport (pierredup) + * bug #33172 [Console] fixed a PHP notice when there is no function in the stack trace of an Exception (fabpot) + * bug #33157 Fix getMaxFilesize() returning zero (ausi) + * bug #33139 [Intl] Cleanup unused language aliases entry (ro0NL) + * bug #33126 [SecurityBundle] display the correct class name on the deprecated notice (maxhelias) + * bug #33093 [EventDispatcher] wrong Request class (maxhelias) + * bug #33092 [DependencyInjection] Improve an exception message (fabpot) + * bug #32541 [HttpKernel] trim the leading backslash in the controller init (Simperfit, fabpot) + * bug #32455 [HttpFoundation] Clear invalid session cookie (Toflar) + * bug #33066 [Serializer] Fix negative DateInterval (jderusse) + * bug #33045 Make HttpClientTestCase compatible with PHPUnit8 (jderusse) + * bug #33033 [Lock] consistently throw NotSupportException (xabbuh) + * bug #33022 [HttpClient] Remove CURLOPT_CONNECTTIMEOUT_MS curl opt (lyrixx) + * bug #32516 [FrameworkBundle][Config] Ignore exceptions thrown during reflection classes autoload (fancyweb) + * bug #33010 [TwigBridge] pass translation parameters to the trans filter (xabbuh) + * bug #32981 Fix tests/code for php 7.4 (jderusse) + * bug #32986 [Mime] fixed wrong mimetype (rjwebdev) + * bug #32992 [ProxyManagerBridge] Polyfill for unmaintained version (jderusse) + * bug #32989 [HttpClient] Declare `$active` first to prevent weird issue (Kocal) + * bug #32999 Added correct plural for box -> boxes (cinamo) + * bug #32933 [PhpUnitBridge] fixed PHPUnit 8.3 compatibility: method handleError was renamed to __invoke (karser) + * bug #32947 [Intl] Support DateTimeInterface in IntlDateFormatter::format (pierredup) + * bug #32919 [Intl] Order alpha2 to alpha3 mapping + phpdoc fixes (ro0NL) + * bug #32792 [Messenger] Fix incompatibility with FrameworkBundle <4.3.1 (chalasr) + * bug #32836 [Messenger] Removed named parameters and replaced with `?` placeholders for sqlsrv compatibility (David Legatt) + * bug #32838 [FrameworkBundle] Detect indirect env vars in routing (ro0NL) + * bug #32918 [Intl] Order alpha2 to alpha3 mapping (ro0NL) + * bug #32902 [PhpUnitBridge] Allow sutFqcnResolver to return array (VincentLanglet) + * bug #32814 Create mailBody with only attachments part present (srsbiz) + * bug #32682 [HttpFoundation] Revert getClientIp @return docblock (ossinkine) + * bug #32910 [Yaml] PHP-8: Uncaught TypeError: abs() expects parameter 1 to be int or float, string given (Aleksandr Dankovtsev) + * bug #32870 #32853 Check if $this->parameters is array. (ABGEO07) + * bug #32899 [Mailer] fix wrong error message when connection closes unexpectedly (fabpot) + * bug #32895 [Mailer] Fix error not being thrown properly (fabpot) + * bug #32868 [PhpUnitBridge] Allow symfony/phpunit-bridge > 4.2 to be installed with phpunit 4.8 (jderusse) + * bug #32823 [HttpClient] Preserve the case of headers when sending them (nicolas-grekas) + * bug #32767 [Yaml] fix comment in multi line value (soufianZantar) + * bug #32790 [HttpFoundation] Fix `getMaxFilesize` (bennyborn) + * bug #32796 [Cache] fix warning on PHP 7.4 (jpauli) + * bug #32806 [Console] fix warning on PHP 7.4 (rez1dent3) + * bug #32809 Don't add object-value of static properties in the signature of container metadata-cache (arjenm) + * bug #32708 Recompile container when translations directory changes (pierredup) + * bug #32722 [DependencyInjection] Fix bindings and tagged_locator (deguif) + * bug #32802 Make sure trace_level is always defined (dbu) + * bug #30096 [DI] Fix dumping Doctrine-like service graphs (bis) (weaverryan, nicolas-grekas) + * bug #32799 [HttpKernel] do not stopwatch sections when profiler is disabled (Tobion) + * bug #32631 [Messenger] expire delay queue and fix auto_setup logic (Tobion) + * bug #32641 [Messenger] Retrieve table default options from the SchemaManager (vincenttouzet) + +* 4.3.3 (2019-07-28) + + * bug #32726 [Messenger] Fix redis last error not cleared between calls (chalasr) + * bug #32760 [HttpKernel] clarify error handler restoring process (xabbuh) + * bug #32730 [Inflector] Fix pluralizing words ending with "son" (norkunas) + * bug #32715 [DI] fix perf issue with lazy autowire error messages (nicolas-grekas) + * bug #32503 Fix multiSelect ChoiceQuestion when answers have spaces (IceMaD) + * bug #32688 [Yaml] fix inline handling when dumping tagged values (xabbuh) + * bug #32710 [Security/Core] align defaults for sodium with PHP 7.4 (nicolas-grekas) + * bug #32644 [WebProfileBundle] Avoid getting right to left style (Arman-Hosseini) + * bug #32689 [HttpClient] rewind stream when using Psr18Client (nicolas-grekas) + * bug #32700 [Messenger] Flatten collection of stamps collected by the traceable middleware (ogizanagi) + * bug #32699 [HttpClient] fix canceling responses in a streaming loop (nicolas-grekas) + * bug #32679 [Intl] relax some date parser patterns (xabbuh) + * bug #31303 [VarDumper] Use \ReflectionReference for determining if a key is a reference (php >= 7.4) (dorumd, nicolas-grekas) + * bug #32485 [Validator] Added support for validation of giga values (kernig) + * bug #32567 [Messenger] pass transport name to factory (Tobion) + * bug #32568 [Messenger] Fix UnrecoverableExceptionInterface handling (LanaiGrunt) + * bug #32604 Properly handle optional tag attributes for !tagged_iterator (apfelbox) + * bug #32571 [HttpClient] fix debug output added to stderr at shutdown (nicolas-grekas) + * bug #32443 [PHPUnitBridge] Mute deprecations triggered from phpunit (greg0ire) + * bug #32572 Bump minimum version of symfony/phpunit-bridge (fancyweb) + * bug #32438 [Serializer] XmlEncoder: don't cast padded strings (ogizanagi) + * bug #32579 [Config] Do not use absolute path when computing the vendor freshness (lyrixx) + * bug #32563 Container*::getServiceIds() should return strings (mathroc) + * bug #32553 [Mailer] Allow register mailer configuration in xml format (Koc) + * bug #32442 Adding missing event_dispatcher wiring for messenger.middleware.send_message (weaverryan) + * bug #32466 [Config] Fix for signatures of typed properties (tvandervorm) + * bug #32501 [FrameworkBundle] Fix descriptor of routes described as callable array (ribeiropaulor) + * bug #32500 [Debug][DebugClassLoader] Include found files instead of requiring them (fancyweb) + * bug #32464 [WebProfilerBundle] Fix Twig 1.x compatibility (yceruto) + * bug #31620 [FrameworkBundle] Inform the user when save_path will be ignored (gnat42) + * bug #32096 Don't assume port 0 for X-Forwarded-Port (alexbowers, xabbuh) + * bug #31820 [SecurityBundle] Fix profiler dump for non-invokable security listeners (chalasr) + * bug #32392 [Messenger] Doctrine Transport: Support setting auto_setup from DSN (bendavies) + * bug #31267 [Translator] Load plurals from mo files properly (Stadly) + * bug #31266 [Translator] Load plurals from po files properly (Stadly) + * bug #32383 [Serializer] AbstractObjectNormalizer ignores the property types of discriminated classes (sandergo90) + * bug #32413 [Messenger] fix publishing headers set on AmqpStamp (Tobion) + * bug #32421 [EventDispatcher] Add tag kernel.rest on 'debug.event_dispatcher' service (lyrixx) + * bug #32398 [Messenger] Removes deprecated call to ReflectionType::__toString() on MessengerPass (brunowowk) + * bug #32379 [SecurityBundle] conditionally register services (xabbuh) + * bug #32380 [Messenger] fix broken key normalization (Tobion) + * bug #32363 [FrameworkBundle] reset cache pools between requests (nicolas-grekas) + * bug #32365 [DI] fix processing of regular parameter bags by MergeExtensionConfigurationPass (nicolas-grekas) + * bug #32187 [PHPUnit] Fixed composer error on Windows (misterx) + * bug #32299 [Lock]Β Stores must implement `putOffExpiration` (jderusse) + * bug #32302 [Mime] Remove @internal annotations for the serialize methods (francoispluchino) + * bug #32334 [Messenger] Fix authentication for redis transport (alexander-schranz) + * bug #32309 Fixing validation for messenger transports retry_strategy service key (weaverryan) + * bug #32331 [Workflow] only decorate when an event dispatcher was passed (xabbuh) + * bug #32236 [Cache] work aroung PHP memory leak (nicolas-grekas) + * bug #32206 Catch JsonException and rethrow in JsonEncode (phil-davis) + * bug #32211 [Mailer] Fix error message when connecting to a stream raises an error before connect() (fabpot) + * bug #32210 [Mailer] Fix timeout type hint (fabpot) + * bug #32199 [EventDispatcher] improve error messages in the event dispatcher (xabbuh) + * bug #32200 [Security/Core] work around sodium_compat issue (nicolas-grekas) + * 4.3.2 (2019-06-26) * bug #31954 [PhpunitBridge] Read environment variable from superglobals (greg0ire) diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md new file mode 100644 index 0000000000000..7ef838934633d --- /dev/null +++ b/CHANGELOG-4.4.md @@ -0,0 +1,1712 @@ +CHANGELOG for 4.4.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 4.4 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1 + +* 4.4.51 (2023-11-10) + + * security #cve-2023-46734 [TwigBridge] Ensure CodeExtension's filters properly escape their input (nicolas-grekas, GromNaN) + +* 4.4.50 (2023-02-01) + + * security #cve-2022-24895 [Security/Http] Remove CSRF tokens from storage on successful login (nicolas-grekas) + * security #cve-2022-24894 [HttpKernel] Remove private headers before storing responses with HttpCache (nicolas-grekas) + +* 4.4.49 (2022-11-28) + + * bug #48273 [HttpKernel] Fix message for unresovable arguments of invokable controllers (fancyweb) + * bug #48224 [DependencyInjection] Process bindings in `ServiceLocatorTagPass` (MatTheCat) + * bug #48198 [Messenger] Fix time-limit check exception (alamirault) + * bug #48122 [PhpUnitBridge] Fix language deprecations incorrectly marked as direct (wouterj) + * bug #48085 [Messenger] Tell about messenger:consume invalid limit options (MatTheCat) + * bug #48120 [Messenger] Do not throw 'no handlers' exception when skipping handlers due to duplicate handling (wouterj) + * bug #48112 [HttpFoundation] Compare cookie with null value as empty string in ResponseCookieValueSame (fancyweb) + * bug #48119 [FrameworkBundle][Lock] Allow to disable lock without defining a resource (MatTheCat) + * bug #48093 [DependencyInjection] don't move locator tag for service subscriber (RobertMe) + * bug #48075 [Mailer] Stream timeout not detected fgets returns false (Sezil) + * bug #48092 Fix the notification email theme for asynchronously dispatched emails (krisbuist) + * bug #48103 [HttpClient] Do not set http_version instead of setting it to null (Tetragramat) + * bug #48050 [HttpFoundation] Check IPv6 is valid before comparing it (PhilETaylor) + +* 4.4.48 (2022-10-28) + + * bug #47907 [Console] Update Application.php (aleksandr-shevchenko) + * bug #47932 Throw LogicException instead of Error when trying to generate logout-… (addiks) + * bug #47857 [HttpKernel] Fix empty request stack when terminating with exception (krzyc) + * bug #47878 [HttpKernel] Remove EOL when using error_log() in HttpKernel Logger (cyve) + * bug #47883 [Console] Fix error output on windows cli (Maximilian.Beckers) + * bug #47884 [Cache] Reserve numeric keys when doing memory leak prevention (simoheinonen) + * bug #47822 [Mailer] fix: use message object from event (rogamoore) + * bug #47858 [DoctrineBridge] Implement `EventManager::getAllListeners()` (derrabus) + +* 4.4.47 (2022-10-12) + + * bug #47621 [Serializer] Allow getting discriminated type by class name (TamasSzigeti) + * bug #47808 [HttpClient] Fix seeking in not-yet-initialized requests (nicolas-grekas) + * bug #47702 [Messenger] Fix default serializer not handling DateTime objects properly (barton-webwings) + * bug #47779 [Console] Fix `Helper::removeDecoration` hyperlink bug (greew) + * bug #47763 [PropertyInfo] a readonly property must not be reported as being writable (xabbuh) + * bug #47731 [WebProfiler] Fix overflow issue in Forms panel (zolikonta) + * bug #47746 [HttpFoundation] Fix BinaryFileResponse content type detection logic (X-Coder264) + +* 4.4.46 (2022-09-30) + + * bug #47547 [Ldap] Do not run ldap_set_option on failed connection (tatankat) + * bug #47578 [Security] Fix AbstractFormLoginAuthenticator return types (AndrolGenhald) + * bug #47614 [FrameworkBundle] Fix a phpdoc in mailer assertions (HeahDude) + * bug #47516 [HttpFoundation] Prevent BinaryFileResponse::prepare from adding content type if no content is sent (naitsirch) + * bug #47533 [Messenger] decode URL-encoded characters in DSN's usernames/passwords (xabbuh) + * bug #47530 [HttpFoundation] Always return strings from accept headers (ausi) + * bug #47497 [Bridge] Fix mkdir() race condition in ProxyCacheWarmer (andrey-tech) + * bug #47415 [HttpClient] Psr18Client ignore invalid HTTP headers (nuryagdym) + * bug #47435 [HttpKernel] lock when writting profiles (nicolas-grekas) + * bug #47437 [Mime] Fix email rendering when having inlined parts that are not related to the content (fabpot) + * bug #47434 [HttpFoundation] move flushing outside of Response::closeOutputBuffers (nicolas-grekas) + * bug #47351 [FrameworkBundle] Do not throw when describing a factory definition (MatTheCat) + * bug #47403 [Mailer] Fix edge cases in STMP transports (fabpot) + +* 4.4.45 (2022-08-26) + + * bug #47358 Fix broken request stack state if throwable is thrown. (Warxcell) + * bug #47304 [Serializer] Fix caching context-aware encoders/decoders in ChainEncoder/ChainDecoder (Guite) + * bug #47329 Email image parts: regex for single closing quote (rr-it) + * bug #47200 [Form] ignore missing keys when mapping DateTime objects to uninitialized arrays (xabbuh) + * bug #47189 [Validator] Add additional hint when `egulias/email-validator` needs to be installed (mpdude) + * bug #47195 [FrameworkBundle] fix writes to static $kernel property (xabbuh) + * bug #47175 [DowCrawler] Fix locale-sensitivity of whitespace normalization (nicolas-grekas) + * bug #47171 [TwigBridge] suggest to install the Twig bundle when the required component is already installed (xabbuh) + * bug #47161 [Mailer] Fix logic (fabpot) + * bug #47157 [Messenger] Fix Doctrine transport on MySQL (nicolas-grekas) + * bug #46190 [Translation] Fix translator overlapse (Xavier RENAUDIN) + * bug #47142 [Mailer] Fix error message in case of an STMP error (fabpot) + * bug #47145 [HttpClient] Fix shared connections not being freed on PHP < 8 (nicolas-grekas) + * bug #47143 [HttpClient] Fix memory leak when using StreamWrapper (nicolas-grekas) + * bug #47130 [HttpFoundation] Fix invalid ID not regenerated with native PHP file sessions (BrokenSourceCode) + +* 4.4.44 (2022-07-29) + + * bug #47069 [Security] Allow redirect after login to absolute URLs (Tim Ward) + * bug #47073 [HttpKernel] Fix non-scalar check in surrogate fragment renderer (aschempp) + * bug #43329 [Serializer] Respect default context in DateTimeNormalizer::denormalize (hultberg) + * bug #47086 Workaround disabled "var_dump" (nicolas-grekas) + * bug #40828 [BrowserKit] Merge fields and files recursively if they are multidimensional array (januszmk) + * bug #47048 [Serializer] Fix XmlEncoder encoding attribute false (alamirault) + * bug #47000 [ErrorHandler] Fix return type patching for list and class-string pseudo types (derrabus) + * bug #43998 [HttpKernel] [HttpCache] Don't throw on 304 Not Modified (aleho) + * bug #46981 [Mime] Β quote address names if they contain parentheses (xabbuh) + * bug #46960 [FrameworkBundle] Fail gracefully when forms use disabled CSRF (HeahDude) + * bug #46973 [DependencyInjection] Fail gracefully when attempting to autowire composite types (derrabus) + * bug #46963 [Mime] Fix inline parts when added via attachPart() (fabpot) + * bug #46968 [PropertyInfo] Make sure nested composite types do not crash ReflectionExtractor (derrabus) + * bug #46931 Flush backend output buffer after closing. (bradjones1) + * bug #46905 [BrowserKit] fix sending request to paths containing multiple slashes (xabbuh) + * bug #42033 [HttpFoundation] Fix deleteFileAfterSend on client abortion (nerg4l) + * bug #46941 [Messenger] Fix calls to deprecated DBAL methods (derrabus) + * bug #46863 [Mime] Fix invalid DKIM signature with multiple parts (BrokenSourceCode) + * bug #46808 [HttpFoundation] Fix TypeError on null `$_SESSION` in `NativeSessionStorage::save()` (chalasr) + * bug #46790 [HttpFoundation] Prevent PHP Warning: Session ID is too long or contains illegal characters (BrokenSourceCode) + * bug #46800 Spaces in system temp folder path cause deprecation errors in php 8 (demeritcowboy) + * bug #46797 [Messenger] Ceil waiting time when multiplier is a float on retry (WissameMekhilef) + +* 4.4.43 (2022-06-26) + + * bug #46765 [Serializer] Fix denormalization union types with constructor (Gwemox) + * bug #46769 [HttpKernel] Fix a PHP 8.1 deprecation notice in HttpCache (mpdude) + * bug #46747 Fix global state pollution between tests run with ApplicationTester (Seldaek) + * bug #46730 [Intl] Fix the IntlDateFormatter::formatObject signature (damienalexandre) + * bug #46668 [FrameworkBundle] Lower JsonSerializableNormalizer priority (aprat84) + * bug #46678 [HttpFoundation] Update "[Session] Overwrite invalid session id" to only validate when files session storage is used (alexpott) + * bug #45861 [Serializer] Try all possible denormalization route with union types when ALLOW_EXTRA_ATTRIBUTES=false (T-bond) + * bug #46676 [DoctrineBridge] Extend type guessing on enum fields (Gigino Chianese) + * bug #46699 [Cache] Respect $save option in all adapters (jrjohnson) + * bug #46697 [HttpKernel] Disable session tracking while collecting profiler data (nicolas-grekas) + * bug #46684 [MonologBridge] Fixed support of elasticsearch 7.+ in ElasticsearchLogstashHandler (lyrixx) + * bug #46368 [Mailer] Fix for missing sender name in case with usage of the EnvelopeListener (bobahvas) + * bug #46548 [Mime] Allow url as a path in the DataPart::fromPath (wkania) + * bug #46594 [FrameworkBundle] Fix XML cache config (HeahDude) + * bug #46595 [Console] Escape in command name & description from getDefaultName() (ogizanagi) + * bug #46565 [WebProfilerBundle] Fix dark theme selected line highlight color & reuse css vars (ogizanagi) + * bug #46535 [Mime] Check that the path is a file in the DataPart::fromPath (wkania) + * bug #46543 [Cache] do not pass null to strlen() (xabbuh) + * bug #46478 [Contracts] remove static cache from `ServiceSubscriberTrait` (kbond) + +* 4.4.42 (2022-05-27) + + * bug #46448 [DependencyInjection] Fix "proxy" tag: resolve its parameters and pass it to child definitions (nicolas-grekas) + * bug #46442 [FrameworkBundle] Revert "bug #46125 Always add CacheCollectorPass (fancyweb)" (chalasr) + * bug #46443 [DoctrineBridge] Don't reinit managers when they are proxied as ghost objects (nicolas-grekas) + * bug #46427 [FrameworkBundle] fix wiring of annotations.cached_reader (nicolas-grekas) + * bug #46434 [FrameworkBundle] Fix BC break in abstract config commands (yceruto) + * bug #46424 [Form] do not accept array input when a form is not multiple (xabbuh) + * bug #46367 [Mime] Throw exception when body in Email attach method is not ok (alamirault) + * bug #46421 [VarDumper][VarExporter] Deal with DatePeriod->include_end_date on PHP 8.2 (nicolas-grekas) + * bug #46401 [Cache] Throw when "redis_sentinel" is used with a non-Predis "class" option (buffcode) + * bug #46414 Bootstrap 4 fieldset for row errors (konradkozaczenko) + * bug #46412 [FrameworkBundle] Fix dumping extension config without bundle (yceruto) + * bug #46407 [Filesystem] Safeguard (sym)link calls (derrabus) + * bug #46098 [Form] Fix same choice loader with different choice values (HeahDude) + * bug #46380 [HttpClient] Add missing HttpOptions::setMaxDuration() (nicolas-grekas) + * bug #46249 [HttpFoundation] [Session] Regenerate invalid session id (peter17) + * bug #46366 [Mime] Add null check for EmailHeaderSame (magikid) + * bug #46364 [Config] Fix looking for single files in phars with GlobResource (nicolas-grekas) + * bug #46365 [HttpKernel] Revert "bug #46327 Allow ErrorHandler ^5.0 to be used" (nicolas-grekas) + * bug #46114 Fixes "Incorrectly nested style tag found" error when using multi-line header content (Perturbatio) + * bug #46325 [Ldap] Fix LDAP connection options (buffcode) + * bug #46317 [Security/Http] Ignore invalid URLs found in failure/success paths (nicolas-grekas) + * bug #46327 [HttpKernel] Allow ErrorHandler ^5.0 to be used in HttpKernel 4.4 (mpdude) + * bug #46297 [Serializer] Fix JsonSerializableNormalizer ignores circular reference handler in $context (BreyndotEchse) + * bug #45981 [Serializer][PropertyInfo] Fix support for "false" built-in type on PHP 8.2 (alexandre-daubois) + * bug #46277 [HttpKernel] Fix SessionListener without session in request (edditor) + * bug #46282 [DoctrineBridge] Treat firstResult === 0 like null (derrabus) + * bug #46278 [Workflow] Fix deprecated syntax for interpolated strings (nicolas-grekas) + * bug #46264 [Console] Better required argument check in InputArgument (jnoordsij) + * bug #46262 [EventDispatcher] Fix removing listeners when using first-class callable syntax (javer) + * bug #46216 [Form] fix populating single widget time view data with different timezones (xabbuh) + * bug #46221 [DomCrawler][VarDumper] Fix html-encoding emojis (nicolas-grekas) + * bug #46167 [VarExporter] Fix exporting DateTime objects on PHP 8.2 (nicolas-grekas) + +* 4.4.41 (2022-04-27) + + * bug #46154 [Mailer] Restore X-Transport after failure (zenas1210) + * bug #46171 [VarDumper] Fix dumping floats on PHP8 (nicolas-grekas) + * bug #46170 Fix dumping enums on PHP 8.2 (nicolas-grekas) + * bug #46143 [Cache] Prevent fatal errors on php 8 when running concurrently with TagAwareAdapter v6.1 (sbelyshkin) + * bug #46149 Modify processing of uploaded files to be compatible with PHP 8.1 (p-golovin) + * bug #46125 [FrameworkBundle] Always add CacheCollectorPass (fancyweb) + * bug #46121 Fix "Notice: Undefined index: headers" in messenger with Oracle (rjd22) + * bug #45980 [Finder] Add support of no-capture regex modifier in MultiplePcreFilterIterator (available from PHP 8.2) (alexandre-daubois) + * bug #46008 [Workflow] Catch error when trying to get an uninitialized marking (lyrixx) + * bug #40998 [Form] Use reference date in reverse transform (KDederichs) + * bug #46012 [HttpKernel] Fix Symfony not working on SMB share (qinshuze) + * bug #45992 [Mailer] Return-Path has higher priority for envelope address than From address (tpetry) + * bug #45998 [HttpClient] Fix sending content-length when streaming the body (nicolas-grekas) + * bug #45565 Fix table header seperator wrapping (alamirault) + * bug #45968 [Intl] Update the ICU data to 71.1 - 4.4 (jderusse) + * bug #45947 [FrameworkBundle] [Command] Fix `debug:router --no-interaction` error … (WilliamBoulle) + * bug #45931 [Process] Fix Process::getEnv() when setEnv() hasn't been called before (asika32764) + * bug #45928 [ExpressionLanguage] Fix matching null against a regular expression (ausi) + +* 4.4.40 (2022-04-02) + + * bug #45910 [Messenger] reset connection on worker shutdown (SanderHagen) + * bug #45909 [Form][TwigBundle] reset Twig form theme resources between requests (xabbuh) + * bug #45906 [HttpClient] on redirections don't send content related request headers (xabbuh) + * bug #45714 [Messenger] Fix cannot select FOR UPDATE from view on Oracle (rjd22) + * bug #45888 [Messenger] Add mysql indexes back and work around deadlocks using soft-delete (nicolas-grekas) + * bug #45891 [HttpClient] Fix exporting objects with readonly properties (nicolas-grekas) + * bug #45875 [ExpressionLanguage] Fix matches when the regexp is not valid (fabpot) + * bug #45870 [Validator] Fix File constraint invalid max size exception message (fancyweb) + * bug #45851 [Console] Fix exit status on uncaught exception with negative code (acoulton) + * bug #45838 [Serializer] Fix denormalizing union types (T-bond) + * bug #45816 [Mailer] Preserve case of headers (nicolas-grekas) + * bug #45814 [HttpClient] Let curl handle Content-Length headers (nicolas-grekas) + * bug #45813 [HttpClient] Move Content-Type after Content-Length (nicolas-grekas) + * bug #45737 [Lock] SemaphoreStore catching exception from sem_get (Triplkrypl) + * bug #45690 [Mailer] Use recipients in sendmail transport (HypeMC) + * bug #45720 [PropertyInfo] strip only leading `\` when unknown docType (EmilMassey) + * bug #44915 [Console] Fix compact table style to avoid outputting a leading space (Seldaek) + * bug #45676 [Process] Don't return executable directories in PhpExecutableFinder (fancyweb) + * bug #45702 [Form] Fix the usage of the Valid constraints in array-based forms (stof) + * bug #45677 [DependencyInjection] fix `ServiceSubscriberTrait` bug where parent has `__call()` (kbond) + * bug #45678 [HttpClient] Fix reading proxy settings from dotenv when curl is used (nicolas-grekas) + * bug #45671 [FrameworkBundle] Ensure container is reset between tests (nicolas-grekas) + +* 4.4.39 (2022-03-05) + + * bug #45631 [HttpFoundation] Fix PHP 8.1 deprecation in `Response::isNotModified` (HypeMC) + * bug #45610 [HttpKernel] Guard against bad profile data (nicolas-grekas) + * bug #45532 Fix deprecations on PHP 8.2 (nicolas-grekas) + * bug #45595 [FrameworkBundle] Fix resetting container between tests (nicolas-grekas) + * bug #45585 [HttpClient] fix checking for unset property on PHP <= 7.1.4 (nicolas-grekas) + * bug #45583 [WebProfilerBundle] Fixes HTML syntax regression introduced by #44570 (xavismeh) + +* 4.4.38 (2022-02-28) + + * bug #44570 [WebProfilerBundle] add nonces to profiler (garak) + * bug #44839 MailerInterface: failed exception contract when enabling messenger (Giorgio Premi) + * bug #45529 [DependencyInjection] Don't reset env placeholders during compilation (nicolas-grekas) + * bug #45527 [HttpClient] Fix overriding default options with null (nicolas-grekas) + * bug #45531 [Serializer] Fix passing null to str_contains() (Erwin Dirks) + * bug #42458 [Validator][Tests] Fix AssertingContextualValidator not throwing on remaining expectations (fancyweb) + * bug #45496 [VarDumper] Fix dumping mysqli_driver instances (nicolas-grekas) + * bug #45495 [HttpFoundation] Fix missing ReturnTypeWillChange attributes (luxemate) + * bug #45482 [Cache] Add missing log when saving namespace (developer-av) + * bug #45479 [HttpKernel] Reset services between requests performed by KernelBrowser (nicolas-grekas) + * bug #44650 [Serializer] Make document type nodes ignorable (boenner) + * bug #45469 [SecurityBundle] fix autoconfiguring Monolog's ProcessorInterface (nicolas-grekas) + * bug #45414 [FrameworkBundle] KernelTestCase resets internal state on tearDown (core23) + * bug #45460 [Intl] fix wrong offset timezone PHP 8.1 (Lenny4) + * bug #45462 [HttpKernel] Fix extracting controller name from closures (nicolas-grekas) + * bug #45424 [DependencyInjection] Fix type binding (sveneld) + * bug #44259 [Security] AccountStatusException::$user should be nullable (Cantepie) + * bug #45323 [Serializer] Fix ignored callbacks in denormalization (benjaminmal) + * bug #45399 [FrameworkBundle] Fix sorting bug in sorting of tagged services by priority (Ahummeling) + * bug #45338 [Mailer] Fix string-cast of exceptions thrown by authenticator in EsmtpTransport (wikando-ck) + * bug #45339 [Cache] fix error handling when using Redis (nicolas-grekas) + * bug #45281 [Cache] Fix connecting to Redis via a socket file (alebedev80) + * bug #45289 [FrameworkBundle] Fix log channel of TagAwareAdapter (fancyweb) + * bug #45306 [PropertyAccessor] Add missing TypeError catch (b1rdex) + * bug #44868 [DependencyInjection][FrameworkBundle] Fix using PHP 8.1 enum as parameters (ogizanagi) + * bug #45261 [HttpClient] Fix Content-Length header when possible (nicolas-grekas) + * bug #45258 [DependencyInjection] Don't dump polyfilled classes in preload script (nicolas-grekas) + * bug #38534 [Serializer] make XmlEncoder stateless thus reentrant (connorhu) + * bug #42253 [Form] Do not fix URL protocol for relative URLs (bogkonstantin) + * bug #45256 [DomCrawler] ignore bad charsets (nicolas-grekas) + * bug #45255 [PropertyAccess] Fix handling of uninitialized property of parent class (filiplikavcan) + * bug #45204 [Validator] Fix minRatio and maxRatio when getting rounded (alexander-schranz) + * bug #45240 [Console] Revert StringInput bc break from #45088 (bobthecow) + +* 4.4.37 (2022-01-28) + + * bug #44939 [Form] UrlType should not add protocol to emails (GromNaN) + * bug #43149 Silence warnings during tty detection (neclimdul) + * bug #45181 [Console] Fix PHP 8.1 deprecation in ChoiceQuestion (BrokenSourceCode) + * bug #45140 [Yaml] Making the parser stateless (mamazu) + * bug #45103 [Process] Avoid calling fclose on an already closed resource (Seldaek) + * bug #45088 [Console] fix parsing escaped chars in StringInput (nicolas-grekas) + * bug #45096 [Cache] Throw exception if incompatible version of psr/simple-cache is used (colinodell) + * bug #45063 [DependencyInjection] remove arbitratry limitation to exclude inline services from bindings (nicolas-grekas) + * bug #44986 [DependencyInjection] copy synthetic status when resolving child definitions (kbond) + * bug #45073 [HttpClient] Fix Failed to open stream: Too many open files (adrienfr) + * bug #45053 [Console] use STDOUT/ERR in ConsoleOutput to save opening too many file descriptors (nicolas-grekas) + * bug #45029 [Cache] Set mtime of cache files 1 year into future if they do not expire (Blacksmoke16) + * bug #45012 [DoctrineBridge] Fix invalid guess with enumType (jderusse) + * bug #45015 [HttpClient] fix resetting DNS/etc when calling CurlHttpClient::reset() (nicolas-grekas) + * bug #44890 [HttpClient] Remove deprecated usage of `GuzzleHttp\Promise\queue` (GrahamCampbell) + * bug #45002 [PropertyAccess] Fix handling of uninitialized property of anonymous class (filiplikavcan) + * bug #44979 [DependencyInjection] Add iterable to possible binding type (vladimir.panivko) + * bug #44976 [FrameworkBundle] Avoid calling rtrim(null, '/') in AssetsInstallCommand (pavol-tk, GromNaN) + * bug #44879 [DependencyInjection] Ignore argument type check in CheckTypeDeclarationsPass if it's a Definition with a factory (fancyweb) + * bug #44931 Allow a zero time-limit for messenger:consume (fritzmg) + * bug #44932 [DependencyInjection] Fix nested env var with resolve processor (Laurent Moreau) + * bug #44912 [Console] Allow OutputFormatter::escape() to be used for escaping URLs used in (Seldaek) + * bug #44878 [HttpClient] Turn negative timeout to a very long timeout (fancyweb) + * bug #44854 [Validator] throw when Constraint::_construct() has not been called (nicolas-grekas) + +* 4.4.36 (2021-12-29) + + * bug #44838 [DependencyInjection][HttpKernel] Fix enum typed bindings (ogizanagi) + * bug #44826 [HttpKernel] Do not attempt to register enum arguments in controller service locator (ogizanagi) + * bug #44820 [Cache] Don't lock when doing 8000 nested computations (nicolas-grekas) + * bug #44807 [Messenger] fix Redis support on 32b arch (nicolas-grekas) + * bug #44759 [HttpFoundation] Fix notice when HTTP_PHP_AUTH_USER passed without pass (Vitali Tsyrkin) + * bug #44799 [Cache] fix compat with apcu < 5.1.10 (nicolas-grekas) + * bug #44732 [Mime] Relaxing in-reply-to header validation (ThomasLandauer) + * bug #44728 [Mime] Fix encoding filenames in multipart/form-data (nicolas-grekas) + * bug #44710 [DependencyInjection] fix linting callable classes (nicolas-grekas) + * bug #44639 [DependencyInjection] Cast tag attribute value to string (ruudk) + * bug #44473 [Validator] Restore default locale in ConstraintValidatorTestCase (rodnaph) + * bug #44577 [Cache] Fix proxy no expiration to the Redis (Sergey Belyshkin) + * bug #44669 [Cache] disable lock on CLI (nicolas-grekas) + * bug #44537 [Config] In XmlUtils, avoid converting from octal every string starting with a 0 (alexandre-daubois) + * bug #44625 [HttpClient] fix monitoring responses issued before reset() (nicolas-grekas) + * bug #44623 [HttpClient] Fix dealing with "HTTP/1.1 000 " responses (nicolas-grekas) + * bug #44601 [HttpClient] Fix closing curl-multi handle too early on destruct (nicolas-grekas) + * bug #44571 [HttpClient] Don't reset timeout counter when initializing requests (nicolas-grekas) + * bug #44479 [HttpClient] Double check if handle is complete (Nyholm) + * bug #44418 [DependencyInjection] Resolve ChildDefinition in AbstractRecursivePass (fancyweb) + * bug #43164 [FrameworkBundle] Fix cache pool configuration with one adapter and one provider (fancyweb) + * bug #44538 [Process] fixed uppercase ARGC and ARGV should also be skipped (rbaarsma) + * bug #44438 [HttpClient] Fix handling thrown \Exception in \Generator in MockResponse (fancyweb) + * bug #44502 [HttpFoundation] do not call preg_match() on null (xabbuh) + * bug #44467 [Console] Fix parameter types for `ProcessHelper::mustRun()` (derrabus) + * bug #44399 Prevent infinite nesting of lazy `ObjectManager` instances when `ObjectManager` is reset (Ocramius) + * bug #44375 [DoctrineBridge] fix calling get_class on non-object (kbond) + * bug #44361 [HttpClient] Fix handling error info in MockResponse (fancyweb) + * bug #43876 [Validator] Fix validation for single level domains (HypeMC) + * bug #44327 [Debug][ErrorHandler] Increased the reserved memory from 10k to 32k (sakalys) + * bug #44261 [Process] intersect with getenv() in case-insensitive manner to get default envs (stable-staple) + * bug #44295 [Serializer] fix support for lazy/unset properties (nicolas-grekas) + * bug #44269 [DoctrineBridge] Revert " add support for the JSON type" (dunglas) + +* 4.4.35 (2021-11-24) + + * security #cve-2021-41270 [Serializer] Use single quote to escape formulas (jderusse) + * bug #44232 [Cache] fix connecting to local Redis sockets (nicolas-grekas) + * bug #44204 [HttpClient] fix closing curl multi handle when destructing client (nicolas-grekas) + * bug #44208 [Process] exclude argv/argc from possible default env vars (nicolas-grekas) + +* 4.4.34 (2021-11-22) + + * bug #44188 [VarExporter] fix exporting declared but unset properties when __sleep() is implemented (nicolas-grekas) + * bug #44119 [HttpClient][Mime] Add correct IDN flags for IDNA2008 compliance (j-bernard) + * bug #44131 [Yaml] properly parse quoted strings tagged with !!str (xabbuh) + * bug #42323 [TwigBridge] do not merge label classes into expanded choice labels (xabbuh) + * bug #44121 [Serializer] fix support for lazy properties (nicolas-grekas) + * bug #44111 [Serializer] fix support for unset properties on PHP < 7.4 (nicolas-grekas) + * bug #44070 [Process] intersect with getenv() to populate default envs (nicolas-grekas) + * bug #44043 [Cache] fix dbindex Redis (a1812) + * bug #44042 Fix DateIntervalToStringTransformer::transform() doc (BenMorel) + * bug #44034 [Yaml] don't try to replace references in quoted strings (xabbuh) + * bug #44028 [ErrorHandler] Fix FlattenException::setPrevious argument typing (welcoMattic) + * bug #44012 [DependencyInjection] fix inlining when non-shared services are involved (nicolas-grekas) + * bug #44002 [Cache] Fix Memory leak (a1812) + * bug #43981 [FrameworkBundle] fix registering late resettable services (nicolas-grekas) + * bug #43988 [DoctrineBridge] add support for the JSON type (dunglas) + * bug #43987 [PhpUnitBridge] Fix Uncaught ValueError (dunglas) + * bug #43961 [HttpClient] Curl http client has to reinit curl multi handle on reset (rmikalkenas) + * bug #43922 [DependencyInjection] only allow `ReflectionNamedType` for `ServiceSubscriberTrait` (kbond) + * bug #43901 [SecurityBundle] Default access_decision_manager.strategy option with merge (biozshock) + * bug #43909 [VarExporter] escape unicode chars involved in directionality (nicolas-grekas) + * bug #43867 [VarDumper] Make dumping DateInterval instances timezone-independent (derrabus) + * bug #43096 [Messenger] Use `TransportMessageIdStamp` in `InMemoryTransport` allows retrying (alexndlm) + * bug #43501 [HttpKernel] fix ErrorException in CacheWarmerAggregate (Ahummeling) + * bug #42361 [Translation] correctly handle intl domains with TargetOperation (acran) + * bug #43834 [Inflector] Fix inflector for "zombies" (acodispo) + * bug #43267 [Config] Fix signature generation with nested attributes on PHP 8.1 (agustingomes) + +* 4.4.33 (2021-10-29) + + * bug #43798 [Dotenv] Duplicate $_SERVER values in $_ENV if they don't exist (fancyweb) + * bug #43799 [PhpUnitBridge] fix symlink to bridge in docker by making its path relative (nicolas-grekas) + * bug #43781 [Messenger] Fix `TraceableMessageBus` implementation so it can compute caller even when used within a callback (Ocramius) + * bug #43655 [VarDumper] Fix dumping twig templates found in exceptions (event15) + * bug #43484 [Messenger] Fix Redis Transport when username is empty (villfa) + * bug #43568 [Messenger] fix: TypeError in PhpSerializer::encode() (dsech) + * bug #43591 [Config] Fix files sorting in GlobResource (lyrixx) + * bug #43569 [HttpClient] fix collecting debug info on destruction of CurlResponse (nicolas-grekas) + * bug #43545 [DependencyInjection] fix "url" env var processor (nicolas-grekas) + * bug #43413 [VarDumper] Fix error with uninitialized XMLReader (villfa) + * bug #43388 [Validator] Fixes URL validation for single-char subdomains (DfKimera) + * bug #43333 [HttpClient] fix missing kernel.reset tag on TraceableHttpClient services (nicolas-grekas) + * bug #43302 [Cache] Commit items implicitly only when deferred keys are requested (Sergey Belyshkin) + * bug #43330 [Cache][Lock] fix SQLSRV throws for method_exists() (GDmac) + * bug #43270 [VarDumper] Fix handling of "new" in initializers on PHP 8.1 (nicolas-grekas) + * bug #43277 [DependencyInjection] fix support for "new" in initializers on PHP 8.1 (nicolas-grekas) + * bug #43243 [HttpClient] accept headers when CURLE_RECV_ERROR is received before the content (nicolas-grekas) + * bug #43205 [Serializer] Fix denormalizing XML array with empty body (4.4) (alexandre-daubois) + +* 4.4.32 (2021-09-28) + + * Fix subtree split issues + +* 4.4.31 (2021-09-28) + + * bug #43158 [Cache] Fix invalidating tags on Redis <5 (wouterj) + * bug #43179 [Ldap] Fix `resource` type checks & docblocks on PHP 8.1 (chalasr) + * bug #43137 [FrameworkBundle] Avoid secrets:decrypt-to-local command to fail (noniagriconomie) + * bug #43171 [VarDumper] fix dumping typed references from properties (nicolas-grekas) + * bug #43124 [Messenger] [Redis] Allow authentication with user and password (GaryPEGEOT) + * bug #39350 [FrameworkBundle] Remove translation data_collector BEFORE adding it to profiler (l-vo) + * bug #43115 [DependencyInjection] Fix iterator in ServiceConfigurator (jderusse) + * bug #43031 [Form] Do not trim unassigned unicode characters (simonberger) + * bug #43058 [WebProfilerBundle] Fix displaying certain configs (HypeMC) + * bug #43022 [PhpUnitBridge] Track unsilenced deprecations only for userland (nicolas-grekas) + * bug #42976 [Mime] Allow array as input for RawMessage (derrabus) + * bug #42098 [PropertyInfo] Support for intersection types (derrabus) + * bug #42904 [Cache] Make sure PdoAdapter::prune() always returns a bool (derrabus) + * bug #42896 [HttpClient] Fix handling timeouts when responses are destructed (nicolas-grekas) + * bug #42835 [Cache] Fix implicit float to int cast (derrabus) + * bug #42831 [Mime] Update mime types (fabpot) + * bug #42830 [HttpKernel] Fix empty timeline in profiler (nicodmf) + * bug #42819 Fix tests failing with DBAL 3 (derrabus) + +* 4.4.30 (2021-08-30) + + * bug #42753 Cast ini_get to an integer to match expected type (natewiebe13) + * bug #42345 [Messenger] Remove indices in messenger table on MySQL to prevent deadlocks while removing messages when running multiple consumers (jeroennoten) + * bug #40744 allow null for framework.translator.default_path (SimonHeimberg) + * bug #39856 [DomCrawler] improve failure messages of the CrawlerSelectorTextContains constraint (xabbuh) + * bug #40545 [HttpFoundation] Fix isNotModified determination logic (ol0lll) + * bug #42368 [FrameworkBundle] Fall back to default configuration in debug:config and consistently resolve parameter values (herndlm) + * bug #41684 Fix Url Validator false positives (sidz) + * bug #42576 [Translation] Reverse fallback locales (ro0NL) + * bug #42628 [PropertyInfo] Support for the `never` return type (derrabus) + * bug #42585 [ExpressionLanguage] [Lexer] Remove PHP 8.0 polyfill (nigelmann) + * bug #42621 [Security] Don't produce TypeErrors for non-string CSRF tokens (derrabus) + * bug #42365 [Cache] Do not add namespace argument to `NullAdapter` in `CachePoolPass` (olsavmic) + * bug #42331 [HttpKernel] always close open stopwatch section after handling `kernel.request` events (xabbuh) + * bug #42260 Fix return types for PHP 8.1 (derrabus) + * bug #42341 [Validator] Update MIR card scheme (ossinkine) + +* 4.4.29 (2021-07-29) + + * bug #42307 [Mailer] Fixed decode exception when sendgrid response is 202 (rubanooo) + * bug #42296 [Dotenv][Yaml] Remove PHP 8.0 polyfill (derrabus) + * bug #42289 [HttpFoundation] Fixed type mismatch (Toflar) + +* 4.4.28 (2021-07-27) + + * bug #42270 [WebProfilerBundle] [WebProfiler] "empty" filter bugfix. Filter with name "empty" is not … (luzrain) + +* 4.4.27 (2021-07-26) + + * bug #42212 [Lock] Handle lock with long key (jderusse) + * bug #42223 [Debug][ErrorHandler] Do not use the php80 polyfill (nicolas-grekas) + * bug #42207 [Console] fix table setHeaderTitle without headers (a1812) + * bug #42130 [Translation] fix fallback to Locale::getDefault() (nicolas-grekas) + * bug #42184 [Mailer] Make sure Http TransportException is not leaking (Nyholm) + * bug #42150 [Form] Fix 'invalid_message' use in multiple ChoiceType (alexandre-daubois) + * bug #42174 Indicate compatibility with psr/log 2 and 3 (derrabus) + * bug #42112 [HttpFoundation] fix FileBag under PHP 8.1 (alexpott) + * bug #42131 [PhpUnitBridge] Fix composer resolution on Windows (Rainrider) + * bug #42097 [DependencyInjection] Support for intersection types (derrabus) + * bug #42114 [HttpFoundation] Fix return types of SessionHandler::gc() (derrabus) + * bug #42099 [VarDumper] Support for intersection types (derrabus) + * bug #42011 [Cache] Support decorated Dbal drivers in PdoAdapter (Jeroeny) + * bug #42068 Add a Special Case for Translating Choices in en_US_POSIX (chrisguitarguy) + * bug #42074 Fix ctype_digit deprecation (alexpott) + * bug #42084 [WebProfilerBundle] Fix the values of some CSS properties (javiereguiluz) + * bug #42079 [FrameworkBundle] Fixed file operations in Sodium vault seal (javiereguiluz) + * bug #42054 [DoctrineBridge] fix setting default mapping type to attribute/annotation on php 8/7 respectively (nicolas-grekas) + * bug #42049 [TwigBridge] do not render the same label id attribute twice (xabbuh) + * bug #42032 [HttpKernel] recover from failed deserializations (xabbuh) + * bug #41990 [Lock] fix derivating semaphore from key (nicolas-grekas) + * bug #40529 [Translation] Missing translations from traits (insekticid) + * bug #41384 Fix SkippedTestSuite (jderusse) + * bug #41966 [Console] Revert "bug #41952 fix handling positional arguments" (chalasr, nicolas-grekas) + * bug #41905 [EventDispatcher] Correct the called event listener method case (JJsty1e) + * bug #41952 [Console] fix handling positional arguments (nicolas-grekas) + * bug #41887 [PhpUnitBridge] Fix deprecation handler with PHPUnit 10 (YaFou) + +* 4.4.26 (2021-06-30) + + * bug #41893 [Filesystem] Workaround cannot dumpFile into "protected" folders on Windows (arnegroskurth) + * bug #41665 [HttpKernel] Keep max lifetime also when part of the responses don't set it (mpdude) + * bug #41760 [ErrorHandler] fix handling buffered SilencedErrorContext (nicolas-grekas) + * bug #41807 [HttpClient] fix Psr18Client when allow_url_fopen=0 (nicolas-grekas) + * bug #40857 [DependencyInjection] Add support of PHP enumerations (alexandre-daubois) + * bug #41767 [Config] fix tracking default values that reference the parent class (nicolas-grekas) + * bug #41768 [DependencyInjection] Fix binding "iterable $foo" when using the PHP-DSL (nicolas-grekas) + * bug #41793 [Cache] handle prefixed redis connections when clearing pools (nicolas-grekas) + * bug #41804 [Cache] fix eventual consistency when using RedisTagAwareAdapter with a cluster (nicolas-grekas) + * bug #41773 [Cache] Disable locking on Windows by default (nicolas-grekas) + * bug #41655 [Mailer] fix encoding of addresses using SmtpTransport (dmaicher) + * bug #41663 [HttpKernel] [HttpCache] Keep s-maxage=0 from ESI sub-responses (mpdude) + * bug #41701 [VarDumper] Fix tests for PHP 8.1 (alexandre-daubois) + * bug #41795 [FrameworkBundle] Replace var_export with VarExporter to use array short syntax in secrets list files (alexandre-daubois) + * bug #41779 [DependencyInjection] throw proper exception when decorating a synthetic service (nicolas-grekas) + * bug #41776 [ErrorHandler] [DebugClassLoader] Do not check Phake mocks classes (adoy) + * bug #41780 [PhpUnitBridge] fix handling the COMPOSER_BINARY env var when using simple-phpunit (Taluu) + * bug #41670 [HttpFoundation] allow savePath of NativeFileSessionHandler to be null (simon.chrzanowski) + * bug #41644 [Config] fix tracking attributes in ReflectionClassResource (nicolas-grekas) + * bug #41621 [Process] Fix incorrect parameter type (bch36) + * bug #41624 [HttpClient] Revert bindto workaround for unaffected PHP versions (derrabus) + * bug #41549 [Security] Fix opcache preload with alias classes (jderusse) + * bug #41491 [Serializer] Do not allow to denormalize string with spaces only to valid a DateTime object (sidz) + * bug #41386 [Console] Escape synopsis output (jschaedl) + * bug #41495 [HttpFoundation] Add ReturnTypeWillChange to SessionHandlers (nikic) + +* 4.4.25 (2021-06-01) + + * bug #41000 [Form] Use !isset for checks cause this doesn't falsely include 0 (Kai Dederichs) + * bug #41407 [DependencyInjection] keep container.service_subscriber tag on the decorated definition (xabbuh) + * bug #40866 [Filesystem] fix readlink() for Windows (a1812) + * bug #41394 [Form] fix support for years outside of the 32b range on x86 arch (nicolas-grekas) + * bug #39847 [Messenger] Fix merging PrototypedArrayNode associative values (svityashchuk) + * bug #41346 [WebProfilerBundle] Wrapping exception js in Sfjs check and also loading base_js Sfjs if needed (weaverryan) + +* 4.4.24 (2021-05-19) + + * security #cve-2021-21424 [Security\Core] Fix user enumeration via response body on invalid credentials (chalasr) + * bug #41230 [FrameworkBundle][Validator] Fix deprecations from Doctrine Annotations+Cache (derrabus) + * bug #41240 Fixed deprecation warnings about passing null as parameter (derrabus) + * bug #41241 [Finder] Fix gitignore regex build with "**" (mvorisek) + * bug #41224 [HttpClient] fix adding query string to relative URLs with scoped clients (nicolas-grekas) + * bug #41233 [DependencyInjection][ProxyManagerBridge] Don't call class_exists() on null (derrabus) + * bug #41210 [Console] Fix Windows code page support (orkan) + +* 4.4.23 (2021-05-12) + + * security #cve-2021-21424 [Security][Guard] Prevent user enumeration (chalasr) + * bug #41176 [DependencyInjection] fix dumping service-closure-arguments (nicolas-grekas) + * bug #41168 WDT: Only load "Sfjs" if it is not present already (weaverryan) + * bug #41147 [Inflector][String] wrong plural form of words ending by "pectus" (makraz) + * bug #41160 [HttpClient] Don't prepare the request in ScopingHttpClient (nicolas-grekas) + * bug #40763 Fix/Rewrite .gitignore regex builder (mvorisek) + * bug #40917 [Config][DependencyInjection] Uniformize trailing slash handling (dunglas) + * bug #40699 [PropertyInfo] Make ReflectionExtractor correctly extract nullability (shiftby) + * bug #40874 [PropertyInfo] fix attribute namespace with recursive traits (soullivaneuh) + * bug #41099 [Cache] Check if phpredis version is compatible with stream parameter (nicolassing) + * bug #41072 [VarExporter] Add support of PHP enumerations (alexandre-daubois) + * bug #41105 [Inflector][String] Fixed singularize `edges` > `edge` (ruudk) + * bug #41075 [ErrorHandler] Skip "same vendor" ``@method`` deprecations for `Symfony\*` classes unless symfony/symfony is being tested (nicolas-grekas) + +* 4.4.22 (2021-05-01) + + * bug #40993 [Security] [Security/Core] fix checking for bcrypt (nicolas-grekas) + * bug #40923 [Yaml] expose references detected in inline notation structures (xabbuh) + * bug #40964 [HttpFoundation] Fixes for PHP 8.1 deprecations (jrmajor) + * bug #40514 [Yaml] Allow tabs as separators between tokens (bertramakers) + * bug #40882 [Cache] phpredis: Added full TLS support for RedisCluster (jackthomasatl) + * bug #40793 [DoctrineBridge] Add support for a driver type "attribute" (beberlei) + * bug #40807 RequestMatcher issue when `_controller` is a closure (Plopix) + * bug #40811 [PropertyInfo] Use the right context for methods defined in traits (colinodell) + * bug #40330 [SecurityBundle] Empty line starting with dash under "access_control" causes all rules to be skipped (monteiro) + * bug #40780 [Cache] Apply NullAdapter as Null Object (roukmoute) + * bug #40740 [Cache][FrameworkBundle] Fix logging for TagAwareAdapter (fancyweb) + * bug #40755 [Routing] Better inline requirements and defaults parsing (Foxprodev) + * bug #40754 [PhpUnitBridge] Fix phpunit symlink on Windows (johnstevenson) + * bug #40707 [Yaml] Fixed infinite loop when parser goes through an additional and invalid closing tag (alexandre-daubois) + * bug #40679 [Debug][ErrorHandler] Avoid warning with Xdebug 3 with develop mode disabled (Jean85) + * bug #40702 [HttpClient] allow CurlHttpClient on Windows (n0rbyt3) + * bug #40503 [Yaml] fix parsing some block sequences (a1812) + * bug #40610 Fixed bugs found by psalm (Nyholm) + * bug #40603 [Config] Fixed support for nodes not extending BaseNode (Nyholm) + * bug #40645 [FrameworkBundle] Dont store cache misses on warmup (Nyholm) + * bug #40629 [DependencyInjection] Fix "url" env var processor behavior when the url has no path (fancyweb) + * bug #40655 [Cache] skip storing failure-to-save as misses in ArrayAdapter (nicolas-grekas) + * bug #40522 [Serializer] Allow AbstractNormalizer to use null for non-optional nullable constructor parameters without default value (Pierre Rineau) + * bug #40595 add missing queue_name to find(id) in doctrine messenger transport (monteiro) + +* 4.4.21 (2021-03-29) + + * bug #40598 [Form] error if the input string couldn't be parsed as a date (xabbuh) + * bug #40587 [HttpClient] fix using stream_copy_to_stream() with responses cast to php streams (nicolas-grekas) + * bug #40510 [Form] IntegerType: Always use en for IntegerToLocalizedStringTransformer (Warxcell) + * bug #40593 Uses the correct assignment action for console options depending if they are short or long (topikito) + * bug #40535 [HttpKernel] ConfigDataCollector to return known data without the need of a Kernel (topikito) + * bug #40552 [Translation] Fix update existing key with existing +int-icu domain (Alexis) + * bug #40537 [Security] Handle properly 'auto' option for remember me cookie security (fliespl) + * bug #40506 [Validator] Avoid triggering the autoloader for user-input values (Seldaek) + * bug #40538 [HttpClient] remove using $http_response_header (nicolas-grekas) + * bug #40508 [PhpUnitBridge] fix reporting deprecations from DebugClassLoader (nicolas-grekas) + * bug #40348 [Console] Fix line wrapping for decorated text in block output (grasmash) + * bug #40499 [Inflector][String] Fixed pluralize "coupon" (Nyholm) + * bug #40494 [PhpUnitBridge] fix compat with symfony/debug (nicolas-grekas) + * bug #40453 [VarDumper] Adds support for ReflectionUnionType to VarDumper (Michael Nelson, michaeldnelson) + * bug #40460 Correctly clear lines for multi-line progress bar messages (grasmash) + * bug #40450 [Console] ProgressBar clears too many lines on update (danepowell) + * bug #40178 [FrameworkBundle] Exclude unreadable files when executing About command (michaljusiega) + * bug #40472 [Bridge\Twig] Add 'form-control-range' for range input type (Oviglo) + * bug #39866 [Mime] Escape commas in address names (YaFou) + * bug #40373 Check if templating engine supports given view (fritzmg) + * bug #39992 [Security] Refresh original user in SwitchUserListener (AndrolGenhald) + * bug #40446 [TwigBridge] Fix "Serialization of 'Closure'" error when rendering an TemplatedEmail (jderusse) + * bug #40425 [DoctrineBridge] Fix eventListener initialization when eventSubscriber constructor dispatch an event (jderusse) + * bug #40313 [FrameworkBundle] Fix PropertyAccess definition when not in debug (PedroTroller) + * bug #40417 [Form] clear unchecked choice radio boxes even if clear missing is set to false (xabbuh) + * bug #40388 [ErrorHandler] Added missing type annotations to FlattenException (derrabus) + * bug #40407 [TwigBridge] Allow version 3 of the Twig extra packages (derrabus) + * bug #39685 [Mailer][Mime][TwigBridge][Validator] Allow egulias/email-validator 3.x (derrabus) + * bug #40398 [FrameworkBundle] : Fix method name compare in ResolveControllerNameSubscriber (glensc) + * bug #39733 [TwigBridge] Render email once (jderusse) + * bug #40386 [DependencyInjection][Security] Backport psr/container 1.1/2.0 compatibility (derrabus) + +* 4.4.20 (2021-03-04) + + * bug #40318 [Translation] deal with indented heredoc/nowdoc tokens (xabbuh) + * bug #40350 [DependencyInjection] fix parsing calls of methods named "method" (xabbuh) + * bug #40316 [Serializer] zero parts can be omitted in date interval input (xabbuh) + * bug #40239 MockResponse total_time should not be simulated when provided (Pierrick VIGNAND) + * bug #40299 [Cache] Add server-commands support for Predis Replication Environments (DemigodCode) + * bug #40231 [HttpKernel] Configure `session.cookie_secure` earlier (tamcy) + * bug #40283 [Translation] Make `name` attribute optional in xliff2 (MarieMinasyan) + * bug #39599 [Cache] Fix Redis TLS scheme `rediss` for Redis connection (misaert) + * bug #40244 [Routing] fix conflict with param named class in attribute (nlhommet) + * bug #40273 [Cache] fix setting items' metadata on commit() (nicolas-grekas) + * bug #40258 [Form] Ignoring invalid forms from delete_empty behavior in CollectionType (yceruto) + * bug #40162 [Intl] fix Locale::getFallback() throwing exception on long $locale (AmirHo3ein13) + * bug #40208 [PropertyInfo] fix resolving self to name of the analyzed class (xabbuh) + * bug #40209 [WebLink] Escape double quotes in attributes values (fancyweb) + * bug #40192 [Console] fix QuestionHelper::getHiddenResponse() not working with space in project directory name (Yendric) + * bug #40175 [PropertyInfo] Β use the right context for properties defined in traits (xabbuh) + * bug #40172 [Translation] Allow using dashes in locale when linting Xliff files (localheinz) + * bug #40187 [Console] Fix PHP 8.1 null error for preg_match flag (kylekatarnls) + * bug #39659 [Form] keep valid submitted choices when additional choices are submitted (xabbuh) + * bug #40188 [HttpFoundation] Fix PHP 8.1 null values (kylekatarnls) + * bug #40167 [DependencyInjection] Definition::removeMethodCall should remove all matching calls (ruudk) + * bug #40160 [PropertyInfo] fix extracting mixed type-hinted property types (xabbuh) + * bug #40040 [Finder] Use a lazyIterator to close files descriptors when no longer used (jderusse) + * bug #40135 [FrameworkBundle] Fix freshness checks with boolean parameters on routes (HypeMC) + * bug #40138 [FrameworkBundle] fix registering "annotations.cache" on the "container.hot_path" (nicolas-grekas) + * bug #40116 [FrameworkBundle][Translator] scan directories for translations sequentially (xabbuh) + * bug #40104 [HttpKernel] [Kernel] Silence failed deprecations logs writes (fancyweb) + * bug #40098 [DependencyInjection] fix tracking of changes to vendor/ dirs (nicolas-grekas) + * bug #39980 [Mailer][Mime] Update inline part names with newly generated ContentId (ddegentesh) + * bug #40043 [HttpFoundation] Setting `REQUEST_TIME_FLOAT` when constructing a Request object (ctasada) + * bug #40050 [FrameworkBundle][Translator] Fixed updating catalogue metadata from Intl domain (yceruto) + * bug #40089 [SecurityBundle] role_names variable instead of roles (wickedOne) + * bug #40042 [Doctrine] Restore priority for EventSubscribers (jderusse) + * bug #40066 [ErrorHandler] fix parsing return types in DebugClassLoader (nicolas-grekas) + * bug #40065 [ErrorHandler] fix handling messages with null bytes from anonymous classes (nicolas-grekas) + * bug #40067 [PhpUnitBridge] fix reporting deprecations when they come from DebugClassLoader (nicolas-grekas) + * bug #40060 fix validator when we have false returned by the current element of the iterator (FabienSalles) + * bug #40062 [Mime] Fix case-sensitive handling of header names (piku235) + * bug #40023 [Finder] Β use proper keys to not override appended files (xabbuh) + * bug #40019 [ErrorHandler] Fix strpos error when trying to call a method without a name (Deuchnord) + * bug #40004 [Serializer] Prevent access to private properties without getters (julienfalque) + +* 4.4.19 (2021-01-27) + + * bug #38900 [Serializer] Exclude non-initialized properties accessed with getters (BoShurik) + * bug #39887 [Translator] fix handling plural for floating numbers (kylekatarnls) + * bug #39967 [Messenger] fix redis messenger options with dsn (Kleinast) + * bug #39970 [Messenger] Fix transporting non-UTF8 payloads by encoding them using base 64 (nicolas-grekas) + * bug #39909 [PhpUnitBridge] Allow relative path to composer cache (jderusse) + * bug #39944 [HttpKernel] Configure the ErrorHandler even when it is overriden (nicolas-grekas) + * bug #39932 [Console] [Command] Fix Closure code binding when it is a static anonymous function (fancyweb) + * bug #39880 [DoctrineBridge] Add username to UserNameNotFoundException (qurben) + * bug #39633 [HttpFoundation] Drop int return type from parseFilesize() (LukeTowers) + * bug #39889 [HttpClient] Add check for constant in Curl client (pierredup) + * bug #39886 [HttpFoundation] Revert #38614 and add assert to avoid regressions (BafS) + * bug #39858 Fix problem when SYMFONY_PHPUNIT_VERSION is empty string value (alexander-schranz) + * bug #39861 [DependencyInjection] Skip deprecated definitions in CheckTypeDeclarationsPass (chalasr) + * bug #39862 [Security] Replace message data in JSON security error response (wouterj) + * bug #39667 [DoctrineBridge] Take into account that indexBy="person_id" could be a db column name, for a referenced entity (victormacko) + * bug #39799 [DoctrineBridge] Fix circular loop with EntityManager (jderusse) + * bug #39821 [DependencyInjection] Don't trigger notice for deprecated aliases pointing to deprecated definitions (chalasr) + * bug #39816 [HttpFoundation] use atomic writes in MockFileSessionStorage (nicolas-grekas) + * bug #39735 [Serializer] Rename normalize param (VincentLanglet) + * bug #39797 Dont allow unserializing classes with a destructor (jderusse) + * bug #39743 [Mailer] Fix missing BCC recipients in SES bridge (jderusse) + * bug #39764 [Config] Β fix handling float-like key attribute values (xabbuh) + * bug #39787 [Yaml] a colon followed by spaces exclusively separates mapping keys and values (xabbuh) + * bug #39788 [Cache] fix possible collision when writing tmp file in filesystem adapter (nicolas-grekas) + * bug #39794 Dont allow unserializing classes with a destructor - 4.4 (jderusse) + * bug #39747 [DependencyInjection] Support PHP 8 builtin types in CheckTypeDeclarationsPass (derrabus) + * bug #39738 [VarDumper] fix mutating $GLOBALS while cloning it (nicolas-grekas) + * bug #39746 [DependencyInjection] Fix InvalidParameterTypeException for function parameters (derrabus) + * bug #39681 [HttpFoundation] parse cookie values containing the equal sign (xabbuh) + * bug #39716 [DependencyInjection] do not break when loading schemas from network paths on Windows (xabbuh) + * bug #39703 [Finder] apply the sort callback on the whole search result (xabbuh) + * bug #39717 [TwigBridge] Remove full head content in HTML to text converter (pupaxxo) + * bug #39708 [WebProfilerBundle] take query and request parameters into account when matching routes (xabbuh) + * bug #39683 [Yaml] keep trailing newlines when dumping multi-line strings (xabbuh) + * bug #39670 [Form] disable error bubbling by default when inherit_data is configured (xabbuh) + * bug #39686 [Lock] Fix config merging in lock (jderusse) + * bug #39668 [Yaml] do not dump extra trailing newlines for multiline blocks (xabbuh) + * bug #39653 [Form] fix passing null $pattern to IntlDateFormatter (nicolas-grekas) + * bug #39598 [Messenger] Fix stopwach usage if it has been reset (lyrixx) + * bug #39631 [VarDumper] Fix display of nullable union return types (derrabus) + * bug #39629 [VarDumper] fixed displaying "mixed" as "?mixed" (nicolas-grekas) + * bug #39597 [Mailer] Handle failure when sending DATA (jderusse) + * bug #39610 [ProxyManagerBridge] fix PHP notice, switch to "friendsofphp/proxy-manager-lts" (nicolas-grekas) + +* 4.4.18 (2020-12-18) + + * bug #39531 [Mailer] Fix parsing Dsn with empty user/password (OskarStark) + * bug #39518 [Ldap] Incorrect determination of RelativeDistinguishedName for the "move" operation (astepin) + * bug #39476 [Lock] Prevent store exception break combined store (dzubchik) + * bug #39433 [Cache] fix setting "read_timeout" when using Redis (nicolas-grekas) + * bug #39420 [Cache] Prevent notice on case matching metadata trick (bastnic) + * bug #39203 [DI] Fix not working if only "default_index_method" used (malteschlueter) + * bug #39142 [Config] Stop treating multiline resources as globs (michaelKaefer) + * bug #39341 [Form] Fixed StringUtil::trim() to trim ZERO WIDTH SPACE (U+200B) and SOFT HYPHEN (U+00AD) (pmishev) + * bug #39334 [Config][TwigBundle] Fixed syntax error in config (Nyholm) + * bug #39196 [DI] Fix Xdebug 3.0 detection (vertexvaar) + * bug #39226 [PhpUnitBridge] Fix disabling DeprecationErrorHandler from PHPUnit configuration file (fancyweb) + * bug #39357 [FrameworkBundle] fix preserving some special chars in the query string (nicolas-grekas) + * bug #39271 [HttpFoundation] Fix TypeError: Argument 1 passed to JsonResponse::setJson() must be of the type string, object given (sidz) + * bug #39251 [DependencyInjection] Fix container linter for union types (derrabus) + * bug #39336 [Config] YamlReferenceDumper: No default value required for VariableNode with array example (Nyholm) + * bug #39333 [Form] do not apply the Valid constraint on scalar form data (lchrusciel, xabbuh) + * bug #39331 [PhpUnitBridge] Fixed PHPunit 9.5 compatibility (wouterj) + * bug #39220 [HttpKernel] Fix bug with whitespace in Kernel::stripComments() (ausi) + * bug #39252 [Mime] Leverage PHP 8's detection of CSV files (derrabus) + * bug #39313 [FrameworkBundle] TextDescriptor::formatControllerLink checked method… (fjogeleit) + * bug #39286 [HttpClient] throw clearer error when no scheme is provided (BackEndTea) + * bug #39267 [Yaml] fix lexing backslashes in single quoted strings (xabbuh) + * bug #39151 [DependencyInjection] Fixed incorrect report for private services if required service does not exist (Islam93) + * bug #39274 [Yaml] fix lexing mapping values with trailing whitespaces (xabbuh) + * bug #39270 [Inflector] Fix Notice when argument is empty string (moldman) + * bug #39247 [Security] remove return type definition in order to avoid type juggling (adeptofvoltron) + * bug #39223 [Console] Re-enable hyperlinks in Konsole/Yakuake (OndraM) + * bug #39241 [Yaml] fix lexing inline sequ 8000 ences/mappings with trailing whitespaces (Nyholm, xabbuh) + * bug #39243 [Filesystem] File existence check before calling unlink method (gechetspr) + +* 4.4.17 (2020-11-29) + + * bug #39166 [Messenger] Fix mssql compatibility for doctrine transport. (bill moll) + * bug #39211 [HttpClient] fix binding to network interfaces (nicolas-grekas) + * bug #39129 [DependencyInjection] Fix circular in DI with lazy + byContruct loop (jderusse) + * bug #39068 [DependencyInjection][Translator] Silent deprecation triggered by libxml_disable_entity_loader (jderusse) + * bug #39119 [Form] prevent duplicated error message for file upload limits (xabbuh) + * bug #39099 [Form] ignore the pattern attribute for textareas (xabbuh) + * bug #39154 [Yaml] fix lexing strings containing escaped quotation characters (xabbuh) + * bug #38597 [PhpUnitBridge] Fix qualification of deprecations triggered by the debug class loader (fancyweb) + * bug #39160 [Console] Use a partial buffer in SymfonyStyle (jderusse) + * bug #39168 [Console] Fix console closing tag (jderusse) + * bug #39155 [VarDumper] fix casting resources turned into objects on PHP 8 (nicolas-grekas) + * bug #39115 [HttpClient] don't fallback to HTTP/1.1 when HTTP/2 streams break (nicolas-grekas) + * bug #33763 [Yaml] fix lexing nested sequences/mappings (xabbuh) + * bug #39083 [Dotenv] Check if method inheritEnvironmentVariables exists (Chi-teck) + * bug #39094 [Ldap] Fix undefined variable $con (derrabus) + * bug #39091 [Config] Recheck glob brace support after GlobResource was serialized (wouterj) + * bug #39092 Fix critical extension when reseting paged control (jderusse) + * bug #38614 [HttpFoundation] Fix for virtualhosts based on URL path (mvorisek) + * bug #38387 [Validator] prevent hash collisions caused by reused object hashes (fancyweb, xabbuh) + * bug #38999 [DependencyInjection] autoconfigure behavior describing tags on decorators (xabbuh) + * bug #39058 [DependencyInjection] Fix circular detection with multiple paths (jderusse) + * bug #39059 [Filesystem] fix cleaning up tmp files when dumpFile() fails (nicolas-grekas) + * bug #38628 [DoctrineBridge] indexBy could reference to association columns (juanmiguelbesada) + * bug #39021 [DependencyInjection] Optimize circular collection by removing flattening (jderusse) + * bug #39031 [Ldap] Fix pagination (jderusse) + * bug #39038 [DoctrineBridge] also reset id readers (xabbuh) + * bug #39025 [DoctrineBridge] Fix DBAL deprecations in middlewares (derrabus) + * bug #38991 [Console] Fix ANSI when stdErr is not a tty (jderusse) + * bug #38980 [DependencyInjection] Fix circular reference with Factory + Lazy Iterrator (jderusse) + * bug #38971 [PhpUnitBridge] fix replaying skipped tests (nicolas-grekas) + * bug #38910 [HttpKernel] Fix session initialized several times (jderusse) + * bug #38882 [DependencyInjection] Improve performances in CircualReference detection (jderusse) + * bug #38950 [Process] Dont test TTY if there is no TTY support (Nyholm) + * bug #38921 [PHPUnitBridge] Fixed crash on Windows with PHP 8 (villfa) + * bug #38869 [SecurityBundle] inject only compatible token storage implementations for usage tracking (xabbuh) + * bug #38894 [HttpKernel] Remove Symfony 3 compatibility code (derrabus) + * bug #38895 [PhpUnitBridge] Fix wrong check for exporter in ConstraintTrait (alcaeus) + * bug #38879 [Cache] Fixed expiry could be int in ChainAdapter due to race conditions (phamviet) + * bug #38856 [Cache] Add missing use statement (fabpot) + +* 4.4.16 (2020-10-28) + + * bug #38713 [DI] Fix Preloader exception when preloading a class with an unknown parent/interface (rgeraads) + * bug #38647 [HttpClient] relax auth bearer format requirements (xabbuh) + * bug #38699 [DependencyInjection] Preload classes with union types correctly (derrabus) + * bug #38669 [Serializer] fix decoding float XML attributes starting with 0 (Marcin Kruk) + * bug #38680 [PhpUnitBridge] Support new expect methods in test case polyfill (alcaeus) + * bug #38681 [PHPUnitBridge] Support PHPUnit 8 and PHPUnit 9 in constraint compatibility trait (alcaeus) + * bug #38679 [PhpUnitBridge] Add missing exporter function for PHPUnit 7 (alcaeus) + * bug #38595 [TwigBridge] do not translate null placeholders or titles (xabbuh) + * bug #38635 [Cache] Use correct expiry in ChainAdapter (Nyholm) + * bug #38652 [Filesystem] Check if failed unlink was caused by permission denied (Nyholm) + * bug #38645 [PropertyAccess]Β forward the caught exception (xabbuh) + * bug #38604 [DoctrineBridge] indexBy does not refer to attributes, but to column names (xabbuh) + * bug #38606 [WebProfilerBundle] Hide debug toolbar in print view (jt2k) + * bug #38582 [DI] Fix Reflection file name with eval()\'d code (maxime-aknin) + * bug #38516 [HttpFoundation] Fix Range Requests (BattleRattle) + * bug #38553 [Lock] Reset Key lifetime time before we acquire it (Nyholm) + * bug #38551 Remove content-type check on toArray methods (jderusse) + * bug #38544 [DI] fix dumping env vars (nicolas-grekas) + * bug #38530 [HttpClient] fix reading the body after a ClientException (nicolas-grekas) + * bug #38510 [PropertyInfo] Support for the mixed type (derrabus) + * bug #38493 [HttpClient] Fix CurlHttpClient memory leak (HypeMC) + * bug #38456 [Cache] skip igbinary < 3.1.6 (nicolas-grekas) + * bug #38392 [Ldap] Bypass the use of `ldap_control_paged_result` on PHP >= 7.3 (lucasaba) + * bug #38444 [PhpUnitBridge] fix running parallel tests with phpunit 9 (nicolas-grekas) + * bug #38442 [VarDumper] fix truncating big arrays (nicolas-grekas) + * bug #38433 [Mime] Fix serialization of RawMessage (gilbertsoft) + +* 4.4.15 (2020-10-04) + + * bug #36291 [Lock] Fix StoreFactory to accept same DSN syntax as AbstractAdapter (Jontsa) + * bug #38390 [Serializer][Minor] Fix circular reference exception message (bad limit displayed) (l-vo) + * bug #38388 [HttpClient] Always "buffer" empty responses (nicolas-grekas) + * bug #38380 [Form] propagate validation groups to subforms (johanderuijter, xabbuh) + * bug #38377 Ignore more deprecations for Mockery mocks (fancyweb) + * bug #38375 [HttpClient] fix using proxies with NativeHttpClient (nicolas-grekas) + * bug #38372 [Routing] fix using !important and defaults/reqs in inline route definitions (nicolas-grekas) + * bug #38373 [ErrorHandler][DebugClassLoader] Do not check Mockery mocks classes (fancyweb) + * bug #38368 [HttpClient] Fix using https with proxies (bohanyang) + * bug #38350 [TwigBundle] Only remove kernel exception listener if twig is used (dmolineus) + * bug #38360 [BrowserKit] Cookie expiration at current timestamp (iquito) + * bug #38358 [Messenger] Fix redis connection error message (alexander-schranz) + * bug #38343 Revert "bug #38063 [FrameworkBundle] generate preload.php in src/ to make opcache.preload predictable" (nicolas-grekas) + * bug #38336 [PhpUnitBridge] Fixed class_alias() for PHPUnit\Framework\Error\Error (stevegrunwell) + +* 4.4.14 (2020-09-27) + + * bug #38248 [HttpClient] Allow bearer token with colon (stephanvierkant) + * bug #37837 [Form] Fix custom formats deprecation with HTML5 widgets (fancyweb) + * bug #38285 [Contracts][Translation] Optional Intl dependency (ro0NL) + * bug #38283 [Translator] Optional Intl dependency (ro0NL) + * bug #38271 [ErrorHandler] Escape JSON encoded log context (ro0NL) + * bug #38284 [Cache][Lock][Messenger] fix compatibility with Doctrine DBAL 3 (xabbuh) + * bug #38228 [Yaml Parser] Fix edge cases when parsing multiple documents (digilist) + * bug #38229 [Yaml] fix parsing comments not prefixed by a space (xabbuh) + * bug #38127 [Translator] Make sure a null locale is handled properly (jschaedl) + * bug #38221 [Cache] Allow cache tags to be objects implementing __toString() (lstrojny) + * bug #38212 [HttpKernel] Do not override max_redirects option in HttpClientKernel (dmolineus) + * bug #38215 [HttpClient] Support for CURLOPT_LOCALPORT (derrabus) + * bug #38202 [FrameworkBundle] Fix xsd definition which prevent to add more than one workflow metadata (l-vo) + * bug #38166 [Console] work around disabled putenv() (SenTisso) + * bug #38173 [HttpClient][HttpClientTrait] don't calculate alternatives if option is auth_ntlm (ybenhssaien) + * bug #38169 [PhpUnitBridge] Internal classes are not legacy (derrabus) + * bug #38156 [Cache] fix ProxyAdapter not persisting items with infinite expiration (dmaicher) + * bug #38148 [HttpClient] fail properly when the server replies with HTTP/0.9 (nicolas-grekas) + * bug #38131 [Validator] allow consumers to mock all methods (xabbuh) + * bug #38139 [DI] dump OS-indepent paths in the compiled container (nicolas-grekas) + * bug #38126 [Cache] Limit cache version character range (lstrojny) + * bug #38142 [FrameworkBundle] adopt src/.preload.php (nicolas-grekas) + * bug #38108 [Cache] Fix key encoding issue in Memcached adapter (lstrojny) + * bug #38122 [HttpClient] Fix Array to string conversion notice when parsing JSON error body with non-scalar detail property (emarref) + * bug #37097 DateTime validator support for trailing data (stefankleff) + * bug #38116 [Console] Silence warnings on sapi_windows_cp_set() call (chalasr) + * bug #38114 [Console] guard $argv + $token against null, preventing unnecessary exceptions (bilogic) + * bug #38094 [PhpUnitBridge] Skip internal classes in CoverageListenerTrait (sanmai) + * bug #38101 [VarExporter] unserialize() might throw an Exception on php 8 (derrabus) + * bug #38100 [ErrorHandler] Parse "x not found" errors correctly on php 8 (derrabus) + * bug #38099 Prevent parsing invalid octal digits as octal numbers (julienfalque) + * bug #38095 [Mailer] Remove unnecessary check for existing request (jschaedl) + * bug #38091 [DI] fix ContainerBuilder on PHP8 (nicolas-grekas) + * bug #38086 [HttpClient] with "bindto" with NativeHttpClient (nicolas-grekas) + * bug #38063 [FrameworkBundle] generate preload.php in src/ to make opcache.preload predictable (nicolas-grekas) + * bug #38080 [Console] Make sure $maxAttempts is an int or null (derrabus) + * bug #38075 esmtp error not being thrown properly (Anton Zagorskii) + * bug #38040 [Yaml Parser] fixed Parser to skip comments when inlining sequences (korve) + * bug #38073 [VarDumper] Fix caster for invalid SplFileInfo objects on php 8 (derrabus) + * bug #38071 [PhpUnitBridge] Adjust output parsing of CoverageListenerTrait for PHPUnit 9.3 (sanmai, derrabus) + * bug #38062 [DI] fix generating preload file when cache_dir is outside project_dir (nicolas-grekas) + * bug #38059 [Cache] Fix CacheCollectorPass with decorated cache pools (shyim) + * bug #38054 [PhpUnitBridge] CoverageListenerTrait update for PHPUnit 8.5/9.x (sanmai) + * bug #38049 [Debug] Parse "x not found" errors correctly on php 8 (derrabus) + * bug #38041 [PropertyInfo] Fix typed collections in PHP 7.4 (ndench) + * bug #37959 [PhpunitBridge] Fix deprecation type detection (when several autoload files are used) (l-vo) + +* 4.4.13 (2020-09-02) + + * security #cve-2020-15094 Remove headers with internal meaning from HttpClient responses (mpdude) + * bug #38024 [Console] Fix undefined index for inconsistent command name definition (chalasr) + * bug #38023 [DI] fix inlining of non-shared services (nicolas-grekas) + * bug #38020 [PhpUnitBridge] swallow deprecations (xabbuh) + * bug #38010 [Cache] Psr16Cache does not handle Proxy cache items (alex-dev) + * bug #37937 [Serializer] fixed fix encoding of cache keys with anonymous classes (michaelzangerle) + +* 4.4.12 (2020-08-31) + + * bug #37966 [HttpClient][MockHttpClient][DX] Throw when the response factory callable does not return a valid response (fancyweb) + * bug #37971 [PropertyInfo] Backport support for typed properties (PHP 7.4) (dunglas) + * bug #37970 [PhpUnitBridge] Polyfill new phpunit 9.1 assertions (phpfour) + * bug #37960 [PhpUnit] Add polyfill for assertMatchesRegularExpression() (dunglas) + * bug #37949 [Yaml] fix more numeric cases changing in PHP 8 (xabbuh) + * bug #37921 [Yaml] account for is_numeric() behavior changes in PHP 8 (xabbuh) + * bug #37912 [ExpressionLanguage]Β fix passing arguments to call_user_func_array() on PHP 8 (xabbuh) + * bug #37907 [Messenger] stop using the deprecated schema synchronizer API (xabbuh) + * bug #37900 [Mailer] Fixed mandrill api header structure (wulff) + * bug #37888 [Mailer] Reorder headers used to determine Sender (cvmiert) + * bug #37872 [Sendgrid-Mailer] Fixed envelope recipients on sendgridApiTransport (arendjantetteroo) + * bug #37860 [Serializer][ClassDiscriminatorMapping] Fix getMappedObjectType() when a discriminator child extends another one (fancyweb) + * bug #37853 [Validator] ensure that the validator is a mock object for backwards-compatibility (xabbuh) + * bug #36340 [Serializer] Fix configuration of the cache key (dunglas) + * bug #36810 [Messenger] Do not stack retry stamp (jderusse) + * bug #37849 [FrameworkBundle] Add missing mailer transports in xsd (l-vo) + * bug #37586 [ErrorHandler][DebugClassLoader] Add mixed and static return types support (fancyweb) + * bug #37845 [Serializer] Fix variadic support when using type hints (fabpot) + * bug #37841 [VarDumper] Backport handler lock when using VAR_DUMPER_FORMAT (ogizanagi) + * bug #37725 [Form] Fix Guess phpdoc return type (franmomu) + * bug #37771 Use PHPUnit 9.3 on php 8 (derrabus) + * bug #36140 [Validator] Add BC layer for notInRangeMessage when min and max are set (l-vo) + * bug #35843 [Validator] Add target guards for Composite nested constraints (ogizanagi) + * bug #37803 Fix for issue #37681 (Rav) + * bug #37744 [Yaml] Fix for #36624; Allow PHP constant as first key in block (jnye) + * bug #37767 [Form] fix mapping errors from unmapped forms (xabbuh) + * bug #37731 [Console] Table: support cells with newlines after a cell with colspan >= 2 (GMTA) + * bug #37791 Fix redis connect with empty password (alexander-schranz) + * bug #37790 Fix deprecated libxml_disable_entity_loader (fabpot) + * bug #37763 Fix deprecated libxml_disable_entity_loader (jderusse) + * bug #37774 [Console] Make sure we pass a numeric array of arguments to call_user_func_array() (derrabus) + * bug #37729 [FrameworkBundle] fail properly when the required service is not defined (xabbuh) + * bug #37701 [Serializer] Fix that it will never reach DOMNode (TNAJanssen) + * bug #37671 [Cache] fix saving no-expiry items with ArrayAdapter (philipp-kolesnikov) + * bug #37102 [WebProfilerBundle] Fix error with custom function and web profiler routing tab (JakeFr) + * bug #37560 [Finder] Fix GitIgnore parser when dealing with (sub)directories and take order of lines into account (Jeroeny) + * bug #37700 [VarDumper] Improve previous fix on light array coloration (l-vo) + * bug #37705 [Mailer] Added the missing reset tag to mailer.logger_message_listener (vudaltsov) + * bug #37697 [Messenger] reduce column length for MySQL 5.6 compatibility (xabbuh) + +* 4.4.11 (2020-07-24) + + * bug #37590 Allows RedisClusterProxy instance in Lock RedisStore (jderusse) + * bug #37583 [Mime] Fix EmailHeaderSame to make use of decoded value (evertharmeling) + * bug #37569 [Messenger] Allow same middleware to be used multiple times with different arguments (HypeMC) + * bug #37624 [Cache] Connect to RedisCluster with password auth (mforbak) + * bug #37635 [Cache] fix catching auth errors (nicolas-grekas) + * bug #37628 [Serializer] Support multiple levels of discriminator mapping (jeroennoten) + * bug #37572 [FrameworkBundle] set default session.handler alias if handler_id is not provided (Youssef BENHSSAIEN) + * bug #37607 Fix checks for phpunit releases on Composer 2 (colinodell) + * bug #37594 Use hexadecimal numerals instead of hexadecimals in strings to repres… (arekzb) + * bug #37576 [WebProfilerBundle] modified url generation to use absolute urls (smatyas) + * bug #36888 [Mailer] Fix mandrill raw http request setting from email/name (JohJohan) + * bug #37527 [Mailer] Fix reply-to functionality in the SendgridApiTransport (jt2k) + * bug #37581 [Mime] Fix compat with HTTP requests (fabpot) + * bug #37580 [Mime] Keep Sender full address when used by non-SMTP transports (fabpot) + * bug #37511 [DependencyInjection][Config] Use several placeholder unique prefixes for dynamic placeholder values (fancyweb) + * bug #37562 [Cache] Use the default expiry when saving (not when creating) items (philipp-kolesnikov) + * bug #37563 Fix DBAL deprecation (nicolas-grekas) + * bug #37521 [Form] Fix ChoiceType translation domain (VincentLanglet) + * bug #37550 [OptionsResolver] Fix force prepend normalizer (hason) + * bug #37520 [Form] silently ignore uninitialized properties when mapping data to forms (ph-fritsche) + * bug #37526 [Cache][Config] ensure compatibility with PHP 8 stack traces (xabbuh) + * bug #37513 [PhpUnitBridge] ExcludeList usage for PHPUnit 9.4 (gennadigennadigennadi) + * bug #37461 [Process] Fix Permission Denied error when writing sf_proc_00 lock files on Windows (JasonStephensTAMU) + * bug #37505 [Form] fix handling null as empty data (xabbuh) + * bug #37385 [Console] Fixes question input encoding on Windows (YaFou) + * bug #37491 [HttpClient] Fix promise behavior in HttplugClient (brentybh) + * bug #37469 [Console] always use stty when possible to ask hidden questions (nicolas-grekas) + * bug #37486 [HttpClient] fix parsing response headers in CurlResponse (nicolas-grekas) + * bug #37484 [HttpClient][CurlHttpClient] Fix http_version option usage (fancyweb) + * bug #37447 [Validator] fix validating lazy properties that evaluate to null (xabbuh) + * bug #37464 [ErrorHandler] fix throwing from __toString() (nicolas-grekas) + * bug #37449 [Translation] Fix caching of parent locales file in translator (jvasseur) + * bug #37418 [PhpUnitBridge] Fix compatibility with phpunit 9.3 (Gennadi Janzen) + * bug #37441 [DoctrineBridge] work around Connection::ping() deprecation (nicolas-grekas) + * bug #37291 [MimeType] Duplicated MimeType due to PHP Bug (juanmrad) + * bug #37429 [DI] fix parsing of argument type=binary in xml (Tobion) + * bug #37425 [Form] fix guessing form types for DateTime types (xabbuh) + * bug #37392 [Validator] fix handling typed properties as constraint options (xabbuh) + * bug #37358 Directly use the driverConnection executeUpdate method (TristanPouliquen) + * bug #37389 [HttpFondation] Change file extension of "audio/mpeg" from "mpga" to "mp3" (YaFou) + * bug #37379 [HttpClient] Support for cURL handler objects (derrabus) + * bug #37383 [VarDumper] Support for cURL handler objects (derrabus) + * bug #37395 add .body wrapper element (Nemo64) + * bug #37400 [HttpClient] unset activity list when creating CurlResponse (nicolas-grekas) + * bug #36304 Check whether path is file in DataPart::fromPath() (freiondrej) + * bug #37345 [Form] collect all transformation failures (xabbuh) + * bug #37362 [SecurityBundle] Drop cache.security_expression_language service if invalid (chalasr) + * bug #37353 [DI] disable preload.php on the CLI (nicolas-grekas) + * bug #37268 [Messenger] Fix precedence of DSN options for 4.4 (jderusse) + * bug #37341 Fix support for PHP8 union types (nicolas-grekas) + * bug #37271 [FrameworkBundle] preserve dots in query-string when redirecting (nicolas-grekas) + * bug #37340 Fix support for PHP8 union types (nicolas-grekas) + * bug #37275 [DI] tighten detection of local dirs to prevent false positives (nicolas-grekas) + * bug #37090 [PhpUnitBridge] Streamline ansi/no-ansi of composer according to phpunit --colors option (kick-the-bucket) + * bug #36230 [VarDumper] Fix CliDumper coloration on light arrays (l-vo) + * bug #37270 [FrameworkBundle] preserve dots in query-string when redirecting (nicolas-grekas) + * bug #37319 [HttpClient] Convert CurlHttpClient::handlePush() to instance method (mpesari) + * bug #37342 [Cache] fix compat with DBAL v3 (nicolas-grekas) + * bug #37286 [Console] Reset question validator attempts only for actual stdin (bis) (nicolas-grekas) + * bug #37160 Reset question validator attempts only for actual stdin (ostrolucky) + * bug #36975 [PropertyInfo] Make PhpDocExtractor compatible with phpDocumentor v5 (DerManoMann) + +* 4.4.10 (2020-06-12) + + * bug #37227 [DependencyInjection][CheckTypeDeclarationsPass] Handle unresolved parameters pointing to environment variables (fancyweb) + * bug #37103 [Form] switch the context when validating nested forms (xabbuh) + * bug #37182 [HttpKernel] Fix regression where Store does not return response body correctly (mpdude) + * bug #37193 [DependencyInjection][CheckTypeDeclarationsPass] Always resolve parameters (fancyweb) + * bug #37191 [HttpClient] fix offset computation for data chunks (nicolas-grekas) + * bug #37177 [Ldap] fix refreshUser() ignoring extra_fields (arkste) + * bug #37181 [Mailer] Remove an internal annot (fabpot) + * bug #36913 [FrameworkBundle] fix type annotation on ControllerTrait::addFlash() (ThomasLandauer) + * bug #37162 [Mailer] added the reply-to addresses to the API SES transport request. (ribeiropaulor) + * bug #37167 [Mime] use fromString when creating a new Address (fabpot) + * bug #37169 [Cache] fix forward compatibility with Doctrine DBAL 3 (xabbuh) + * bug #37159 [Mailer] Fixed generator bug when creating multiple transports using Transport::fromDsn (atailouloute) + * bug #37048 [HttpClient] fix monitoring timeouts when other streams are active (nicolas-grekas) + * bug #37085 [Form] properly cascade validation to child forms (xabbuh) + * bug #37095 [PhpUnitBridge] Fix undefined index when output of "composer show" cannot be parsed (nicolas-grekas) + * bug #37092 [PhpUnitBridge] fix undefined var on version 3.4 (nicolas-grekas) + * bug #37065 [HttpClient] Throw JsonException instead of TransportException on empty response in Response::toArray() (jeroennoten) + * bug #37077 [WebProfilerBundle] Move ajax clear event listener initialization on loadToolbar (Bruno BOUTAREL) + * bug #37049 [Serializer] take into account the context when preserving empty array objects (xabbuh) + +* 4.4.9 (2020-05-31) + + * bug #37008 [Security] Fixed AbstractToken::hasUserChanged() (wouterj) + * bug #36894 [Validator] never directly validate Existence (Required/Optional) constraints (xabbuh) + * bug #37007 [Console] Fix QuestionHelper::disableStty() (chalasr) + * bug #36865 [Form] validate subforms in all validation groups (xabbuh) + * bug #36907 Fixes sprintf(): Too few arguments in form transformer (pedrocasado) + * bug #36868 [Validator] Use Mime component to determine mime type for file validator (pierredup) + * bug #37000 Add meaningful message when using ProcessHelper and Process is not installed (l-vo) + * bug #36995 [TwigBridge] fix fallback html-to-txt body converter (nicolas-grekas) + * bug #36993 [ErrorHandler] fix setting $trace to null in FatalError (nicolas-grekas) + * bug #36987 Handle fetch mode deprecation of DBAL 2.11. (derrabus) + * bug #36974 [Security] Fixed handling of CSRF logout error (wouterj) + * bug #36947 [Mime] Allow email message to have "To", "Cc", or "Bcc" header to be valid (Ernest Hymel) + * bug #36914 Parse and render anonymous classes correctly on php 8 (derrabus) + * bug #36921 [OptionsResolver][Serializer] Remove calls to deprecated ReflectionParameter::getClass() (derrabus) + * bug #36920 [VarDumper] fix PHP 8 support (nicolas-grekas) + * bug #36917 [Cache] Accessing undefined constants raises an Error in php8 (derrabus) + * bug #36891 Address deprecation of ReflectionType::getClass() (derrabus) + * bug #36899 [VarDumper] ReflectionFunction::isDisabled() is deprecated (derrabus) + * bug #36905 [Validator] Catch expected ValueError (derrabus) + * bug #36915 [DomCrawler] Catch expected ValueError (derrabus) + * bug #36908 [Cache][HttpClient] Made method signatures compatible with their corresponding traits (derrabus) + * bug #36906 [DomCrawler] Catch expected ValueError (derrabus) + * bug #36904 [PropertyAccess] Parse php 8 TypeErrors correctly (derrabus) + * bug #36839 [BrowserKit] Raw body with custom Content-Type header (azhurb) + * bug #36896 [Config] Removed implicit cast of ReflectionProperty to string (derrabus) + * bug #35944 [Security/Core] Fix wrong roles comparison (thlbaut) + * bug #36882 [PhpUnitBridge] fix installing under PHP >= 8 (nicolas-grekas) + * bug #36833 [HttpKernel] Fix that the `Store` would not save responses with the X-Content-Digest header present (mpdude) + * bug #36867 [PhpUnitBridge] fix bad detection of unsilenced deprecations (nicolas-grekas) + * bug #36862 [Security] Unserialize $parentData, if needed, to avoid errors (rfaivre) + * bug #36855 [HttpKernel] Fix error logger when stderr is redirected to /dev/null (fabpot) + * bug #36838 [HttpKernel] Bring back the debug toolbar (derrabus) + * bug #36592 [BrowserKit] Allow Referer set by history to be overridden (Slamdunk) + * bug #36823 [HttpClient] fix PHP warning + accept status code >= 600 (nicolas-grekas) + * bug #36824 [Security/Core] fix compat of `NativePasswordEncoder` with pre-PHP74 values of `PASSWORD_*` consts (nicolas-grekas) + * bug #36811 [DependencyInjection] Fix register event listeners compiler pass (X-Coder264) + * bug #36789 Change priority of KernelEvents::RESPONSE subscriber (marcw) + * bug #36794 [Serializer] fix issue with PHP 8 (nicolas-grekas) + * bug #36786 [WebProfiler] Remove 'none' when appending CSP tokens (ndench) + * bug #36743 [Yaml] Fix escaped quotes in quoted multi-line string (ossinkine) + * bug #36777 [TwigBundle] FormExtension does not have a constructor anymore since sf 4.0 (Tobion) + * bug #36716 [Mime] handle passing custom mime types as string (mcneely) + * bug #36747 Queue name is a required parameter (theravel) + * bug #36751 [Mime] fix bad method call on `EmailAddressContains` (Kocal) + * bug #36696 [Console] don't check tty on stdin, it breaks with "data lost during stream conversion" (nicolas-grekas) + * bug #36569 [PhpUnitBridge] Mark parent class also covered in CoverageListener (lyrixx) + * bug #36690 [Yaml] prevent notice for invalid octal numbers on PHP 7.4 (xabbuh) + * bug #36590 [Console] Default hidden question to 1 attempt for non-tty session (ostrolucky) + * bug #36497 [Filesystem] Handle paths on different drives (crishoj) + * bug #36678 [WebProfiler] Do not add src-elem CSP directives if they do not exist (ndench) + * bug #36501 [DX] Show the ParseException message in all YAML file loaders (fancyweb) + * bug #36683 [Yaml] fix parse error when unindented collections contain a comment (wdiesveld) + * bug #36672 [Validator] Skip validation when email is an empty object (acrobat) + * bug #36673 [PhpUnitBridge] fix PHP 5.3 compat again (nicolas-grekas) + * bug #36505 [Translation] Fix for translation:update command updating ICU messages (artemoliynyk) + * bug #36627 [Validator] fix lazy property usage. (bendavies) + * bug #36601 [Serializer] do not transform empty \Traversable to Array (soyuka) + * bug #36606 [Cache] Fixed not supported Redis eviction policies (SerheyDolgushev) + * bug #36625 [PhpUnitBridge] fix compat with PHP 5.3 (nicolas-grekas) + +* 4.4.8 (2020-04-28) + + * bug #36536 [Cache] Allow invalidateTags calls to be traced by data collector (l-vo) + * bug #36566 [PhpUnitBridge] Use COMPOSER_BINARY env var if available (fancyweb) + * bug #36560 [YAML] escape DEL(\x7f) (sdkawata) + * bug #36539 [PhpUnitBridge] fix compatibility with phpunit 9 (garak) + * bug #36555 [Cache] skip APCu in chains when the backend is disabled (nicolas-grekas) + * bug #36523 [Form] apply automatically step=1 for datetime-local input (ottaviano) + * bug #36519 [FrameworkBundle] debug:autowiring: Fix wrong display when using class_alias (weaverryan) + * bug #36454 [DependencyInjection][ServiceSubscriber] Support late aliases (fancyweb) + * bug #36498 [Security/Core] fix escape for username in LdapBindAuthenticationProvider.php (stoccc) + * bug #36506 [FrameworkBundle] Fix session.attribute_bag service definition (fancyweb) + * bug #36500 [Routing][PrefixTrait] Add the _locale requirement (fancyweb) + * bug #36457 [Cache] CacheItem with tag is never a hit after expired (alexander-schranz, nicolas-grekas) + * bug #36490 [HttpFoundation] workaround PHP bug in the session module (nicolas-grekas) + * bug #36483 [SecurityBundle] fix accepting env vars in remember-me configurations (zek) + * bug #36343 [Form] Fixed handling groups sequence validation (HeahDude) + * bug #36460 [Cache] Avoid memory leak in TraceableAdapter::reset() (lyrixx) + * bug #36467 Mailer from sender fixes (fabpot) + * bug #36408 [PhpUnitBridge] add PolyfillTestCaseTrait::expectExceptionMessageMatches to provide FC with recent phpunit versions (soyuka) + * bug #36447 Remove return type for Twig function workflow_metadata() (gisostallenberg) + * bug #36449 [Messenger] Make sure redis transports are initialized correctly (Seldaek) + * bug #36411 [Form] RepeatedType should always have inner types mapped (biozshock) + * bug #36441 [DI] fix loading defaults when using the PHP-DSL (nicolas-grekas) + * bug #36434 [HttpKernel] silence E_NOTICE triggered since PHP 7.4 (xabbuh) + * bug #36365 [Validator] Fixed default group for nested composite constraints (HeahDude) + * bug #36422 [HttpClient] fix HTTP/2 support on non-SSL connections - CurlHttpClient only (nicolas-grekas) + * bug #36417 Force ping after transport exception (oesteve) + * bug #35591 [Validator] do not merge constraints within interfaces (greedyivan) + * bug #36377 [HttpClient] Fix scoped client without query option configuration (X-Coder264) + * bug #36387 [DI] fix detecting short service syntax in yaml (nicolas-grekas) + * bug #36392 [DI] add missing property declarations in InlineServiceConfigurator (nicolas-grekas) + * bug #36400 Allowing empty secrets to be set (weaverryan) + * bug #36380 [Process] Fixed input/output error on PHP 7.4 (mbardelmeijer) + * bug #36376 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) + * bug #36375 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) + * bug #36305 [PropertyInfo][ReflectionExtractor] Check the array mutator prefixes last when the property is singular (fancyweb) + * bug #35656 [HttpFoundation] Fixed session migration with custom cookie lifetime (Guite) + * bug #36342 [HttpKernel][FrameworkBundle] fix compat with Debug component (nicolas-grekas) + * bug #36315 [WebProfilerBundle] Support for Content Security Policy style-src-elem and script-src-elem in WebProfiler (ampaze) + * bug #36286 [Validator] Allow URL-encoded special characters in basic auth part of URLs (cweiske) + * bug #36335 [Security] Track session usage whenever a new token is set (wouterj) + * bug #36332 [Serializer] Fix unitialized properties (from PHP 7.4.2) when serializing context for the cache key (alanpoulain) + * bug #36337 [MonologBridge] Fix $level type (fancyweb) + * bug #36223 [Security][Http][SwitchUserListener] Ignore all non existent username protection errors (fancyweb) + * bug #36239 [HttpKernel][LoggerDataCollector] Prevent keys collisions in the sanitized logs processing (fancyweb) + * bug #36245 [Validator] Fixed calling getters before resolving groups (HeahDude) + * bug #36265 Fix the reporting of deprecations in twig:lint (stof) + * bug #36283 [Security] forward multiple attributes voting flag (xabbuh) + +* 4.4.7 (2020-03-30) + + * security #cve-2020-5255 [HttpFoundation] Do not set the default Content-Type based on the Accept header (yceruto) + * security #cve-2020-5275 [Security] Fix access_control behavior with unanimous decision strategy (chalasr) + * bug #36262 [DI] fix generating TypedReference from PriorityTaggedServiceTrait (nicolas-grekas) + * bug #36252 [Security/Http] Allow setting cookie security settings for delete_cookies (wouterj) + * bug #36261 [FrameworkBundle] revert to legacy wiring of the session when circular refs are detected (nicolas-grekas) + * bug #36259 [DomCrawler] Fix BC break in assertions breaking Panther (dunglas) + * bug #36181 [BrowserKit] fixed missing post request parameters in file uploads (codebay) + * bug #36216 [Validator] Assert Valid with many groups (phucwan91) + * bug #36222 [Console] Fix OutputStream for PHP 7.4 (guillbdx) + +* 4.4.6 (2020-03-27) + + * bug #36169 [HttpKernel] fix locking for PHP 7.4+ (nicolas-grekas) + * bug #36175 [Security/Http] Remember me: allow to set the samesite cookie flag (dunglas) + * bug #36173 [Http Foundation] Fix clear cookie samesite (guillbdx) + * bug #36176 [Security] Check if firewall is stateless before checking for session/previous session (koenreiniers) + * bug #36149 [Form] Support customized intl php.ini settings (jorrit) + * bug #36172 [Debug] fix for PHP 7.3.16+/7.4.4+ (nicolas-grekas) + * bug #36151 [Security] Fixed hardcoded value of SODIUM_CRYPTO_PWHASH_MEMLIMIT_INTERACTIVE (lyrixx) + * bug #36141 Prevent warning in proc_open() (BenMorel) + * bug #36143 [FrameworkB 8000 undle] Fix Router Cache (guillbdx) + * bug #36103 [DI] fix preloading script generation (nicolas-grekas) + * bug #36118 [Security/Http] don't require the session to be started when tracking its id (nicolas-grekas) + * bug #36108 [DI] Fix CheckTypeDeclarationPass (guillbdx) + * bug #36121 [VarDumper] fix side-effect by not using mt_rand() (nicolas-grekas) + * bug #36073 [PropertyAccess][DX] Improved errors when reading uninitialized properties (HeahDude) + * bug #36063 [FrameworkBundle] start session on flashbag injection (William Arslett) + * bug #36031 [Console] Fallback to default answers when unable to read input (ostrolucky) + * bug #36083 [DI][Form] Fixed test suite (TimeType changes & unresolved merge conflict) (wouterj) + * bug #36026 [Mime] Fix boundary header (guillbdx) + * bug #36020 [Form] ignore microseconds submitted by Edge (xabbuh) + * bug #36038 [HttpClient] disable debug log with curl 7.64.0 (nicolas-grekas) + * bug #36041 fix import from config file using type: glob (Tobion) + * bug #35987 [DoctrineBridge][DoctrineExtractor] Fix wrong guessed type for "json" type (fancyweb) + * bug #35949 [DI] Fix container lint command when a synthetic service is used in an expression (HypeMC) + * bug #36023 [HttpClient] fix requests to hosts that idn_to_ascii() cannot handle (nicolas-grekas) + * bug #35938 [Form] Handle false as empty value on expanded choices (fancyweb) + * bug #36030 [SecurityBundle] Minor fix in LDAP config tree builder (HeahDude) + * bug #35993 Remove int return type from FlattenException::getCode (wucdbm) + * bug #36004 [Yaml] fix dumping strings containing CRs (xabbuh) + * bug #35982 [DI] Fix XmlFileLoader bad error message (przemyslaw-bogusz) + * bug #35957 [DI] ignore extra tags added by autoconfiguration in PriorityTaggedServiceTrait (nicolas-grekas) + * bug #35937 Revert "bug symfony#28179 [DomCrawler] Skip disabled fields processing in Form" (dmaicher) + * bug #35928 [Routing] Prevent localized routes _locale default & requirement from being overridden (fancyweb) + * bug #35912 [FrameworkBundle] register only existing transport factories (xabbuh) + * bug #35899 [DomCrawler] prevent deprecation being triggered from assertion (xabbuh) + * bug #35910 [SecurityBundle] Minor fixes in configuration tree builder (HeahDude) + +* 4.4.5 (2020-02-29) + + * bug #35781 [Form] NumberToLocalizedStringTransformer return int if scale = 0 (VincentLanglet) + * bug #35846 [Serializer] prevent method calls on null values (xabbuh) + * bug #35897 [FrameworkBundle] add missing Messenger options to XML schema definition (xabbuh) + * bug #35870 [ErrorHandler] fix parsing static return type on interface method annotation (alekitto) + * bug #35839 [Security] Allow switching to another user when already switched (chalasr) + * bug #35851 [DoctrineBridge] Use new Types::* constants and support new json types (fancyweb) + * bug #35716 [PhpUnitBridge] Fix compatibility to PHPUnit 9 (Benjamin) + * bug #35803 [Cache] Fix versioned namespace atomic clears (trvrnrth) + * bug #35817 [DoctrineBridge] Use new Types::* constants and support new json type (fancyweb) + * bug #35832 [Debug][ErrorHandler] improved deprecation notices for methods new args and return type (HeahDude) + * bug #35827 [BrowserKit] Nested file array prevents uploading file (afilina) + * bug #35707 [ExpressionLanguage] Fixed collisions of character operators with object properties (Andrej-in-ua) + * bug #35794 [DoctrineBridge][DoctrineExtractor] Fix indexBy with custom and some core types (fancyweb) + * bug #35787 [PhpUnitBridge] Use trait instead of extending deprecated class (marcello-moenkemeyer) + * bug #35792 [Security] Prevent TypeError in case RememberMetoken has no attached user (nikophil) + * bug #35735 [Routing] Add locale requirement for localized routes (mtarld) + * bug #35772 [Config] don't throw on missing excluded paths (nicolas-grekas) + * bug #35774 [Ldap] force default network timeout (nicolas-grekas) + * bug #35702 [VarDumper] fixed DateCaster not displaying additional fields (Makdessi Alex) + * bug #35722 [HttpKernel] Set previous exception when rethrown from controller resolver (danut007ro) + * bug #35714 [HttpClient] Correctly remove trace level options for HttpCache (aschempp) + * bug #35718 [HttpKernel] fix registering DebugHandlersListener regardless of the PHP_SAPI (nicolas-grekas) + * bug #35728 Add missing autoload calls (greg0ire) + * bug #35693 [Finder] Fix unix root dir issue (chr-hertel) + * bug #35709 [HttpFoundation] fix not sending Content-Type header for 204 responses (Tobion) + * bug #35710 [ErrorHandler] silence warning when zend.assertions=-1 (nicolas-grekas) + * bug #35676 [Console] Handle zero row count in appendRow() for Table (Adam Prickett) + * bug #35696 [Console] Don't load same-namespace alternatives on exact match (chalasr) + * bug #35674 [HttpClient] fix getting response content after its destructor throwed an HttpExceptionInterface (nicolas-grekas) + * bug #35672 [HttpClient] fix HttpClientDataCollector when handling canceled responses (thematchless) + * bug #35641 [Process] throw when PhpProcess::fromShellCommandLine() is used (Guikingone) + * bug #35645 [ErrorHandler] Never throw on warnings triggered by assert() and set assert.exception=1 in Debug::enable() (nicolas-grekas) + * bug #35633 [Mailer] Do not ping the SMTP server before sending every message (micheh) + * bug #33897 [Console] Consider STDIN interactive (ostrolucky) + * bug #35605 [HttpFoundation][FrameworkBundle] fix support for samesite in session cookies (fabpot) + * bug #35609 [DoctrineBridge] Fixed submitting ids with query limit or offset (HeahDude) + * bug #35597 [PHPunit bridge] Provide current file as file path (greg0ire) + * bug #33960 [DI] Unknown env prefix not recognized as such (ro0NL) + * bug #35342 [DI] Fix support for multiple tags for locators and iterators (Alexandre Parent) + * bug #33820 [PhpUnitBridge] Fix some errors when using serialized deprecations (l-vo) + * bug #35553 Fix HTTP client config handling (julienfalque) + * bug #35588 [ErrorHandler] Escape variable in Exception template (jderusse) + * bug #35583 Add missing use statements (fabpot) + * bug #35582 Missing use statement 4.4 (fabpot) + * bug #34123 [Form] Fix handling of empty_data's \Closure value in Date/Time form types (yceruto) + * bug #35537 [Config][XmlReferenceDumper] Prevent potential \TypeError (fancyweb) + * bug #35227 [Mailer] Fix broken mandrill http send for recipients with names (vilius-g) + * bug #35430 [Translation] prefer intl domain when adding messages to catalogue (Guite) + * bug #35497 Fail on empty password verification (without warning on any implementation) (Stefan Kruppa) + * bug #35546 [Validator] check for __get method existence if property is uninitialized (alekitto) + * bug #35332 [Yaml][Inline] Fail properly on empty object tag and empty const tag (fancyweb) + * bug #35489 [PhpUnitBridge] Fix running skipped tests expecting only deprecations (chalasr) + * bug #35161 [FrameworkBundle] Check non-null type for numeric type (Arman-Hosseini) + * bug #34059 [DomCrawler] Skip disabled fields processing in Form (sbogx) + * bug #34114 [Console] SymonfyStyle - Check value isset to avoid PHP notice (leevigraham) + * bug #35557 [Config] dont catch instances of Error (nicolas-grekas) + * bug #35562 [HttpClient] fix HttpClientDataCollector when handling canceled responses (nicolas-grekas) + +* 4.4.4 (2020-01-31) + + * bug #35530 [HttpClient] Fix regex bearer (noniagriconomie) + * bug #35532 [Validator] fix access to uninitialized property when getting value (greedyivan) + * bug #35486 [Translator] Default value for 'sort' option in translation:update should be 'asc' (versgui) + * bug #35305 [HttpKernel]Β Fix stale-if-error behavior, add tests (mpdude) + * bug #34808 [PhpUnitBridge] Properly handle phpunit arguments for configuration file (biozshock) + * bug #35517 [Intl] Provide more locale translations (ro0NL) + * bug #35518 [Mailer] Fix STARTTLS support for Postmark and Mandrill (fabpot) + * bug #35480 [Messenger] Check for all serialization exceptions during message dec… (Patrick Berenschot) + * bug #35502 [Messenger] Fix bug when using single route with XML config (Nyholm) + * bug #35438 [SecurityBundle] fix ldap_bind service arguments (Ioni14) + * bug #35429 [DI] CheckTypeDeclarationsPass now checks if value is type of parameter type (pfazzi) + * bug #35464 [ErrorHandler] Add debug argument to decide whether debug page is shown or not (yceruto) + * bug #35423 Fixes a runtime error when accessing the cache panel (DamienHarper) + * bug #35428 [Cache] fix checking for igbinary availability (nicolas-grekas) + * bug #35424 [HttpKernel] Check if lock can be released (sjadema) + +* 4.4.3 (2020-01-21) + + * bug #35364 [Yaml] Throw on unquoted exclamation mark (fancyweb) + * bug #35065 [Security] Use supportsClass in addition to UnsupportedUserException (linaori) + * bug #35351 Revert #34797 "Fixed translations file dumper behavior" and fix #34713 (yceruto) + * bug #35355 [DI] Fix EnvVar not loaded when Loader requires an env var (jderusse) + * bug #35343 [Security] Fix RememberMe with null password (jderusse) + * bug #34223 [DI] Suggest typed argument when binding fails with untyped argument (gudfar) + * bug #35323 [FrameworkBundle] Set booted flag to false when test kernel is unset (thiagocordeiro) + * bug #35324 [HttpClient] Fix strict parsing of response status codes (Armando-Walmeric) + * bug #35318 [Yaml] fix PHP const mapping keys using the inline notation (xabbuh) + * bug #35306 [FrameworkBundle] Make sure one can use fragments.hinclude_default_template (Nyholm) + * bug #35304 [HttpKernel] Fix that no-cache MUST revalidate with the origin (mpdude) + * bug #35299 Avoid `stale-if-error` in FrameworkBundle's HttpCache if kernel.debug = true (mpdude) + * bug #35240 [SecurityBundle] Fix collecting traceable listeners info on lazy firewalls (chalasr) + * bug #35151 [DI] deferred exceptions in ResolveParameterPlaceHoldersPass (Islam93) + * bug #35290 [Filesystem][FilesystemCommonTrait] Use a dedicated directory when there are no namespace (fancyweb) + * bug #35099 [FrameworkBundle] Do not throw exception on value generate key (jderusse) + * bug #35278 [EventDispatcher]Β expand listener in place (xabbuh) + * bug #35269 [HttpKernel][FileLocator] Fix deprecation message (fancyweb) + * bug #35254 [PHPUnit-Bridge] Fail-fast in simple-phpunit if one of the passthru() commands fails (mpdude) + * bug #35261 [Routing] Fix using a custom matcher & generator dumper class (fancyweb) + * bug #34643 [Dotenv] Fixed infinite loop with missing quote followed by quoted value (naitsirch) + * bug #35239 [Security\Http] Prevent canceled remember-me cookie from being accepted (chalasr) + * bug #35267 [Debug] fix ClassNotFoundFatalErrorHandler (nicolas-grekas) + * bug #35252 [Serializer] Fix cache in MetadataAwareNameConverter (bastnic) + * bug #35200 [TwigBridge] do not render preferred choices as selected (xabbuh) + * bug #35243 [HttpKernel] release lock explicitly (nicolas-grekas) + * bug #35193 [TwigBridge] button_widget now has its title attr translated even if its label = null or false (stephen-lewis) + * bug #35219 [PhpUnitBridge] When using phpenv + phpenv-composer plugin, composer executable is wrapped into a bash script (oleg-andreyev) + * bug #35150 [Messenger] Added check if json_encode succeeded (toooni) + * bug #35137 [Messenger] Added check if json_encode succeeded (toooni) + * bug #35170 [FrameworkBundle][TranslationUpdateCommand] Do not output positive feedback on stderr (fancyweb) + * bug #35245 [HttpClient] fix exception in case of PSR17 discovery failure (nicolas-grekas) + * bug #35244 [Cache] fix processing chain adapter based cache pool (xabbuh) + * bug #35247 [FrameworkBundle][ContainerLintCommand] Only skip .errored. services (fancyweb) + * bug #35225 [DependencyInjection] Handle ServiceClosureArgument for callable in container linting (shieldo) + * bug #35223 [HttpClient] Don't read from the network faster than the CPU can deal with (nicolas-grekas) + * bug #35214 [DI] DecoratorServicePass should keep container.service_locator on the decorated definition (malarzm) + * bug #35209 [HttpClient] fix support for non-blocking resource streams (nicolas-grekas) + * bug #35210 [HttpClient] NativeHttpClient should not send >1.1 protocol version (nicolas-grekas) + * bug #35162 [Mailer] Make sure you can pass custom headers to Mailgun (Nyholm) + * bug #33672 [Mailer] Remove line breaks in email attachment content (Stuart Fyfe) + * bug #35101 [Routing] Fix i18n routing when the url contains the locale (fancyweb) + * bug #35124 [TwigBridge][Form] Added missing help messages in form themes (cmen) + * bug #35195 [HttpClient] fix casting responses to PHP streams (nicolas-grekas) + * bug #35168 [HttpClient] fix capturing SSL certificates with NativeHttpClient (nicolas-grekas) + * bug #35134 [PropertyInfo] Fix BC issue in phpDoc Reflection library (jaapio) + * bug #35184 [Mailer] Payload sent to Sendgrid doesn't include names (versgui) + * bug #35173 [Mailer][MailchimpBridge] Fix missing attachments when sending via Mandrill API (vilius-g) + * bug #35172 [Mailer][MailchimpBridge] Fix incorrect sender address when sender has name (vilius-g) + * bug #35125 [Translator] fix performance issue in MessageCatalogue and catalogue operations (ArtemBrovko) + * bug #35120 [HttpClient] fix scheduling pending NativeResponse (nicolas-grekas) + * bug #35117 [Cache] do not overwrite variable value (xabbuh) + * bug #35113 [VarDumper] Fix "Undefined index: argv" when using CliContextProvider (xepozz) + * bug #34673 Migrate server:log command away from WebServerBundle (jderusse) + * bug #35103 [Translation] Use `locale_parse` for computing fallback locales (alanpoulain) + * bug #35060 [Security] Fix missing defaults for auto-migrating encoders (chalasr) + * bug #35067 [DependencyInjection][CheckTypeDeclarationsPass] Handle \Closure for callable (fancyweb) + * bug #35094 [Console] Fix filtering out identical alternatives when there is a command loader (fancyweb) + +* 4.4.2 (2019-12-19) + + * bug #35051 [DependencyInjection] Fix binding tagged services to containers (nicolas-grekas) + * bug #35039 [DI] skip looking for config class when the extension class is anonymous (nicolas-grekas) + * bug #35049 [ProxyManager] fix generating proxies for root-namespaced classes (nicolas-grekas) + * bug #35022 [Dotenv] FIX missing getenv (mccullagh) + * bug #35023 [HttpKernel] ignore failures generated by opcache.restrict_api (nicolas-grekas) + * bug #35024 [HttpFoundation] fix pdo session handler for sqlsrv (azjezz) + * bug #35025 [HttpClient][Psr18Client] Remove Psr18ExceptionTrait (fancyweb) + * bug #35015 [Config] fix perf of glob discovery when GLOB_BRACE is not available (nicolas-grekas) + * bug #35014 [HttpClient] make pushed responses retry-able (nicolas-grekas) + * bug #35010 [VarDumper] ignore failing __debugInfo() (nicolas-grekas) + * bug #34998 [DI] fix auto-binding service providers to their service subscribers (nicolas-grekas) + * bug #34954 [Mailer] Fixed undefined index when sending via Mandrill API (wulff) + * bug #33670 [DI] Service locators can't be decorated (malarzm) + * bug #35000 [Console][SymfonyQuestionHelper] Handle multibytes question choices keys and custom prompt (fancyweb) + * bug #35005 [HttpClient] force HTTP/1.1 when NTLM auth is used (nicolas-grekas) + * bug #34707 [Validation][FrameworkBundle] Allow EnableAutoMapping to work without auto-mapping namespaces (ogizanagi) + * bug #34996 Fix displaying anonymous classes on PHP 7.4 (nicolas-grekas) + * bug #29839 [Validator] fix comparisons with null values at property paths (xabbuh) + * bug #34900 [DoctrineBridge] Fixed submitting invalid ids when using queries with limit (HeahDude) + * bug #34791 [Serializer] Skip uninitialized (PHP 7.4) properties in PropertyNormalizer and ObjectNormalizer (vudaltsov) + * bug #34956 [Messenger][AMQP] Use delivery_mode=2 by default (lyrixx) + * bug #34915 [FrameworkBundle] Fix invalid Windows path normalization in TemplateNameParser (mvorisek) + * bug #34981 stop using deprecated Doctrine persistence classes (xabbuh) + * bug #34904 [Validator][ConstraintValidator] Safe fail on invalid timezones (fancyweb) + * bug #34935 [FrameworkBundle][DependencyInjection] Skip removed ids in the lint container command and its associated pass (fancyweb) + * bug #34957 [Security] Revert "AbstractAuthenticationListener.php error instead info" (larzuk91) + * bug #34922 [FrameworkBundle][Secrets] Hook configured local dotenv file (fancyweb) + * bug #34967 [HttpFoundation] fix redis multi host dsn not recognized (Jan Christoph Beyer) + * bug #34963 [Lock] fix constructor argument type declaration (xabbuh) + * bug #34955 Require doctrine/persistence ^1.3 (nicolas-grekas) + * bug #34923 [DI] Fix support for immutable setters in CallTrait (Lctrs) + * bug #34878 [TwigBundle] fix broken FilesystemLoader::exists() with Twig 3 (dpesch) + * bug #34921 [HttpFoundation] Removed "Content-Type" from the preferred format guessing mechanism (yceruto) + * bug #34886 [HttpKernel] fix triggering deprecation in file locator (xabbuh) + * bug #34918 [Translation] fix memoryleak in PhpFileLoader (nicolas-grekas) + * bug #34920 [Routing] fix memoryleak when loading compiled routes (nicolas-grekas) + * bug #34787 [Cache] Propagate expiry when syncing items in ChainAdapter (trvrnrth) + * bug #34694 [Validator] Fix auto-mapping constraints should not be validated (ogizanagi) + * bug #34848 [Process] change the syntax of portable command lines (nicolas-grekas) + * bug #34862 [FrameworkBundle][ContainerLintCommand] Reinitialize bundles when the container is reprepared (fancyweb) + * bug #34896 [Cache] fix memory leak when using PhpFilesAdapter (nicolas-grekas) + * bug #34438 [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given (mpdude) + * bug #34449 [Yaml] Implement multiline string as scalar block for tagged values (natepage) + * bug #34601 [MonologBridge] Fix debug processor datetime type (mRoca) + * bug #34842 [ExpressionLanguage] Process division by zero (tigr1991) + * bug #34902 [PropertyAccess] forward caught exception (xabbuh) + * bug #34903 Fixing bad order of operations with null coalescing operator (weaverryan) + * bug #34888 [TwigBundle] add tags before processing them (xabbuh) + * bug #34760 [Mailer] Fix SMTP Authentication when using STARTTLS (DjLeChuck) + * bug #34762 [Config] never try loading failed classes twice with ClassExistenceResource (nicolas-grekas) + * bug #34783 [DependencyInjection] Handle env var placeholders in CheckTypeDeclarationsPass (fancyweb) + * bug #34839 [Cache] fix memory leak when using PhpArrayAdapter (nicolas-grekas) + * bug #34812 [Yaml] fix parsing negative octal numbers (xabbuh) + * bug #34854 [Messenger]Β gracefully handle missing event dispatchers (xabbuh) + * bug #34802 [Security] Check UserInterface::getPassword is not null before calling needsRehash (dbrekelmans) + * bug #34788 [SecurityBundle] Properly escape regex in AddSessionDomainConstraintPass (fancyweb) + * bug #34859 [SecurityBundle] Fix TokenStorage::reset not called in stateless firewall (jderusse) + * bug #34827 [HttpFoundation] get currently session.gc_maxlifetime if ttl doesnt exists (rafaeltovar) + * bug #34755 [FrameworkBundle] resolve service locators in `debug:*` commands (nicolas-grekas) + * bug #34832 [Validator] Allow underscore character "_" in URL username and password (romainneutron) + * bug #34811 [TwigBridge] Update bootstrap_4_layout.html.twig missing switch-custom label (sabruss) + * bug #34820 [FrameworkBundle][SodiumVault] Create secrets directory only when it is used (fancyweb) + * bug #34776 [DI] fix resolving bindings for named TypedReference (nicolas-grekas) + * bug #34794 [DependencyInjection] Resolve expressions in CheckTypeDeclarationsPass (fancyweb) + * bug #34797 [Translation] Fix FileDumper behavior (yceruto) + * bug #34738 [SecurityBundle] Passwords are not encoded when algorithm set to "true" (nieuwenhuisen) + * bug #34759 [SecurityBundle] Fix switch_user provider configuration handling (fancyweb) + * bug #34779 [Security] do not validate passwords when the hash is null (xabbuh) + * bug #34786 [SecurityBundle] Use config variable in AnonymousFactory (martijnboers) + * bug #34784 [FrameworkBundle] Set the parameter bag as resolved in ContainerLintCommand (fancyweb) + * bug #34763 [Security/Core] Fix checking for SHA256/SHA512 passwords (David Brooks) + * bug #34757 [DI] Fix making the container path-independent when the app is in /app (nicolas-grekas) + +* 4.4.1 (2019-12-01) + + * bug #34732 [DependencyInjection][Xml] Fix the attribute 'tag' is not allowed in 'bind' tag (tienvx) + * bug #34729 [DI] auto-register singly implemented interfaces by default (nicolas-grekas) + * bug #34728 [DI] fix overriding existing services with aliases for singly-implemented interfaces (nicolas-grekas) + * bug #34649 more robust initialization from request (dbu) + * bug #34715 [TwigBundle] remove service when base class is missing (xabbuh) + * bug #34600 [DoctrineBridge] do not depend on the QueryBuilder from the ORM (xabbuh) + * bug #34627 [Security/Http] call auth listeners/guards eagerly when they "support" the request (nicolas-grekas) + * bug #34671 [Security] Fix clearing remember-me cookie after deauthentication (chalasr) + * bug #34711 Fix the translation commands when a template contains a syntax error (fabpot) + * bug #34032 [Mime] Fixing multidimensional array structure with FormDataPart (jvahldick) + * bug #34560 [Config][ReflectionClassResource] Handle parameters with undefined constant as their default values (fancyweb) + * bug #34695 [Config] don't break on virtual stack frames in ClassExistenceResource (nicolas-grekas) + * bug #34716 [DependencyInjection] fix dumping number-like string parameters (xabbuh) + * bug #34558 [Console] Fix autocomplete multibyte input support (fancyweb) + * bug #34130 [Console] Fix commands description with numeric namespaces (fancyweb) + * bug #34562 [DI] Skip unknown method calls for factories in check types pass (fancyweb) + * bug #34677 [EventDispatcher] Better error reporting when arguments to dispatch() are swapped (rimas-kudelis) + * bug #33573 [TwigBridge] Add row_attr to all form themes (fancyweb) + * bug #34019 [Serializer] CsvEncoder::NO_HEADERS_KEY ignored when used in constructor (Dario Savella) + * bug #34083 [Form] Keep preferred_choices order for choice groups (vilius-g) + * bug #34091 [Debug] work around failing chdir() on Darwin (mary2501) + * bug #34305 [PhpUnitBridge] Read configuration CLI directive (ro0NL) + * bug #34490 [Serializer] Fix MetadataAwareNameConverter usage with string group (antograssiot) + * bug #34632 [Console] Fix trying to access array offset on value of type int (Tavafi) + * bug #34669 [HttpClient] turn exception into log when the request has no content-type (nicolas-grekas) + * bug #34662 [HttpKernel] Support typehint to deprecated FlattenException in controller (andrew-demb) + * bug #34619 Restores preview mode support for Html and Serializer error renderers (yceruto) + * bug #34636 [VarDumper] notice on potential undefined index (sylvainmetayer) + * bug #34668 [Cache] Make sure we get the correct number of values from redis::mget() (thePanz) + * bug #34621 [Routing] Continue supporting single colon in object route loaders (fancyweb) + * bug #34554 [HttpClient] Fix early cleanup of pushed HTTP/2 responses (lyrixx) + * bug #34607 [HttpKernel] Ability to define multiple kernel.reset tags (rmikalkenas) + * bug #34599 [Mailer][Mailchimp Bridge] Throwing undefined index _id when setting message id (monteiro) + * bug #34569 [Workflow] Apply the same logic of precedence between the apply() and the buildTransitionBlockerList() method (lyrixx) + * bug #34580 [HttpKernel] Don't cache "not-fresh" state (nicolas-grekas) + * bug #34577 [FrameworkBundle][Cache] Don't deep-merge cache pools configuration (alxndrbauer) + * bug #34515 [DependencyInjection] definitions are valid objects (xabbuh) + * bug #34536 [SecurityBundle] Don't require a user provider for the anonymous listener (chalasr) + * bug #34533 [Monolog Bridge] Fixed accessing static property as non static. (Sander-Toonen) + * bug #34502 [FrameworkBundle][ContainerLint] Keep "removing" compiler passes (fancyweb) + * bug #34552 [Dotenv] don't fail when referenced env var does not exist (xabbuh) + * bug #34546 [Serializer] Add DateTimeZoneNormalizer into Dependency Injection (jewome62) + * bug #34547 [Messenger] Error when specified default bus is not among the configured (vudaltsov) + * bug #34513 [Validator] remove return type declaration from __sleep() (xabbuh) + * bug #34551 [Security] SwitchUser is broken when the User Provider always returns a valid user (tucksaun) + * bug #34385 Avoid empty "If-Modified-Since" header in validation request (mpdude) + * bug #34458 [Validator] ConstraintValidatorTestCase: add missing return value to mocked validate method calls (ogizanagi) + * bug #34516 [HttpKernel] drop return type declaration (xabbuh) + * bug #34474 [Messenger] Ignore stamps in in-memory transport (tienvx) + +* 4.4.0 (2019-11-21) + + * bug #34464 [Form] group constraints when calling the validator (nicolas-grekas) + * bug #34451 [DependencyInjection] Fix dumping multiple deprecated aliases (shyim) + * bug #34448 [Form] allow button names to start with uppercase letter (xabbuh) + * bug #34428 [Security] Fix best encoder not wired using migrate_from (chalasr) + +* 4.4.0-RC1 (2019-11-17) + + * bug #34419 [Cache] Disable igbinary on PHP >= 7.4 (nicolas-grekas) + * bug #34347 [Messenger] Perform no deep merging of bus middleware (vudaltsov) + * bug #34366 [HttpFoundation] Allow redirecting to URLs that contain a semicolon (JayBizzle) + * feature #34405 [HttpFoundation] Added possibility to configure expiration time in redis session handler (mantulo) + * bug #34397 [FrameworkBundle] Remove project dir from Translator cache vary scanned directories (fancyweb) + * bug #34384 [DoctrineBridge] Improve queries parameters display in Profiler (fancyweb) + * bug #34408 [Cache] catch exceptions when using PDO directly (xabbuh) + * bug #34411 [HttpKernel] Flatten "exception" controller argument if not typed (chalasr) + * bug #34410 [HttpFoundation] Fix MySQL column type definition. (jbroutier) + * bug #34403 [Cache] Redis Tag Aware warn on wrong eviction policy (andrerom) + * bug #34400 [HttpKernel] collect bundle classes, not paths (nicolas-grekas) + * bug #34398 [Config] fix id-generation for GlobResource (nicolas-grekas) + * bug #34404 [HttpClient] fix HttpClientDataCollector (nicolas-grekas) + * bug #34396 [Finder] Allow ssh2 stream wrapper for sftp (damienalexandre) + * bug #34383 [DI] Use reproducible entropy to generate env placeholders (nicolas-grekas) + * bug #34389 [WebProfilerBundle] add FrameworkBundle requirement (xabbuh) + * bug #34381 [WebProfilerBundle] Require symfony/twig-bundle (fancyweb) + * bug #34358 [Security] always check the token on non-lazy firewalls (nicolas-grekas, lyrixx) + * bug #34390 [FrameworkBundle] fix wiring of httplug client (nicolas-grekas) + * bug #34369 [FrameworkBundle] Disallow WebProfilerBundle < 4.4 (derrabus) + * bug #34370 [DI] fix detecting singly implemented interfaces (nicolas-grekas) + +* 4.4.0-BETA2 (2019-11-13) + + * bug #34344 [Console] Constant STDOUT might be undefined (nicolas-grekas) + * security #cve-2019-18886 [Security\Core] throw AccessDeniedException when switch user fails (nicolas-grekas) + * security #cve-2019-18888 [Mime] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-11325 [VarExporter] fix exporting some strings (nicolas-grekas) + * security #cve-2019-18889 [Cache] forbid serializing AbstractAdapter and TagAwareAdapter instances (nicolas-grekas) + * security #cve-2019-18888 [HttpFoundation] fix guessing mime-types of files with leading dash (nicolas-grekas) + * security #cve-2019-18887 [HttpKernel] Use constant time comparison in UriSigner (stof) + +* 4.4.0-BETA1 (2019-11-12) + + * feature #34333 Revert "feature #34329 [ExpressionLanguage] add XOR operator (ottaviano)" (nicolas-grekas) + * feature #34332 Allow \Throwable $previous everywhere (fancyweb) + * feature #34329 [ExpressionLanguage] add XOR operator (ottaviano) + * feature #34312 [ErrorHandler] merge and remove the ErrorRenderer component (nicolas-grekas, yceruto) + * feature #34309 [HttpKernel] make ExceptionEvent able to propagate any throwable (nicolas-grekas) + * feature #34139 [Security] Add migrating encoder configuration (chalasr) + * feature #32194 [HttpFoundation] Add a way to anonymize IPs (Seldaek) + * feature #34252 [Console] Add support for NO_COLOR env var (Seldaek) + * feature #34295 [DI][FrameworkBundle] add EnvVarLoaderInterface - remove SecretEnvVarProcessor (nicolas-grekas) + * feature #31310 [DependencyInjection] Added option `ignore_errors: not_found` for imported config files (pulzarraider) + * feature #34216 [HttpClient] allow arbitrary JSON values in requests (pschultz) + * feature #31977 Add handling for delayed message to redis transport (alexander-schranz) + * feature #34217 [Messenger] use events consistently in worker (Tobion) + * feature #33065 Deprecate things that prevent \Throwable from bubbling down (fancyweb) + * feature #34184 [VarDumper] display the method we're in when dumping stack traces (nicolas-grekas) + * feature #33732 [Console] Rename some methods related to redraw frequency (javiereguiluz) + * feature #31587 [Routing][Config]Β Allow patterns of resources to be excluded from config loading (tristanbes) + * feature #32256 [DI] Add compiler pass and command to check that services wiring matches type declarations (alcalyn, GuilhemN, nicolas-grekas) + * feature #32061 Add new Form WeekType (dFayet) + * feature #33954 Form theme: support Bootstrap 4 custom switches (romaricdrigon) + * feature #33854 [DI] Add ability to choose behavior of decorations on non existent decorated services (mtarld) + * feature #34185 [Messenger] extract worker logic to listener and get rid of SendersLocatorInterface::getSenderByAlias (Tobion) + * feature #34156 Adding DoctrineClearEntityManagerWorkerSubscriber to reset EM in worker (weaverryan) + * feature #34133 [Cache] add DeflateMarshaller - remove phpredis compression (nicolas-grekas) + * feature #34177 [HttpFoundation][FrameworkBundle] allow configuring the session handler with a DSN (nicolas-grekas) + * feature #32107 [Validator] Add AutoMapping constraint to enable or disable auto-validation (dunglas) + * feature #34170 Re-allow to use "tagged" in service definitions (dunglas) + * feature #34043 [Lock] Add missing lock connection string in FrameworkExtension (jderusse) + * feature #34057 [Lock][Cache] Allows URL DSN in PDO adapters (jderusse) + * feature #34151 [DomCrawler] normalizeWhitespace should be true by default (dunglas) + * feature #34020 [Security] Allow to stick to a specific password hashing algorithm (chalasr) + * feature #34131 [FrameworkBundle] Remove suffix convention when using env vars to override secrets from the vault (nicolas-grekas) + * feature #34051 [HttpClient] allow option "buffer" to be a stream resource (nicolas-grekas) + * feature #34028 [ExpressionLanguage][Lexer] Exponential format for number (tigr1991) + * feature #34069 [Messenger] Removing "sync" transport and replacing it with config trick (weaverryan) + * feature #34014 [DI] made the `env(base64:...)` processor able to decode base64url (nicolas-grekas) + * feature #34044 [HttpClient] Add a canceled state to the ResponseInterface (Toflar) + * feature #33997 [FrameworkBundle] Add `secrets:*` commands and `env(secret:...)` processor to deal with secrets seamlessly (Tobion, jderusse, nicolas-grekas) + * feature #34013 [DI] add `LazyString` for lazy computation of string values injected into services (nicolas-grekas) + * feature #33961 [TwigBridge] Add show-deprecations option to the lint:twig command (yceruto) + * feature #33973 [HttpClient] add HttpClient::createForBaseUri() (nicolas-grekas) + * feature #33980 [HttpClient] try using php-http/discovery when nyholm/psr7 is not installed (nicolas-grekas) + * feature #33967 [Mailer] Add Message-Id to SentMessage when sending an email (fabpot) + * feature #33896 [Serializer][CSV] Add context options to handle BOM (malarzm) + * feature #33883 [Mailer] added ReplyTo option for PostmarkApiTransport (pierregaste) + * feature #33053 [ErrorHandler] Rework fatal errors (fancyweb) + * feature #33939 [Cache] add TagAwareMarshaller to optimize data storage when using AbstractTagAwareAdapter (nicolas-grekas) + * feature #33941 Keeping backward compatibility with legacy FlattenException usage (ycer 8000 uto) + * feature #33851 [EventDispatcher] Allow to omit the event name when registering listeners (derrabus) + * feature #33461 [Cache] Improve RedisTagAwareAdapter invalidation logic & requirements (andrerom) + * feature #33779 [DI] enable improved syntax for defining method calls in Yaml (nicolas-grekas) + * feature #33743 [HttpClient] Async HTTPlug client (Nyholm) + * feature #33856 [Messenger] Allow to configure the db index on Redis transport (chalasr) + * feature #33881 [VarDumper] Added a support for casting Ramsey/Uuid (lyrixx) + * feature #33861 [CssSelector] Support *:only-of-type (jakzal) + * feature #33793 [EventDispatcher] A compiler pass for aliased userland events (derrabus) + * feature #33791 [Form] Added CountryType option for using alpha3 country codes (creiner) + * feature #33628 [DependencyInjection] added Ability to define a priority method for tagged service (lyrixx) + * feature #33775 [Console] Add deprecation message for non-int statusCode (jschaedl) + * feature #33783 [WebProfilerBundle] Try to display the most useful panel by default (fancyweb) + * feature #33701 [HttpKernel] wrap compilation of the container in an opportunistic lock (nicolas-grekas) + * feature #33789 [Serializer] Deprecate the XmlEncoder::TYPE_CASE_ATTRIBUTES constant (pierredup) + * feature #33776 Copy phpunit.xsd to a predictable path (julienfalque) + * feature #31446 [VarDumper] Output the location of calls to dump() (ktherage) + * feature #33412 [Console] Do not leak hidden console commands (m-vo) + * feature #33676 [Security] add "anonymous: lazy" mode to firewalls (nicolas-grekas) + * feature #32440 [DomCrawler] add a normalizeWhitespace argument to text() method (Simperfit) + * feature #33148 [Intl] Excludes locale from language codes (split localized language names) (ro0NL) + * feature #31202 [FrameworkBundle] WebTestCase KernelBrowser::getContainer null return type (Simperfit) + * feature #33038 [ErrorHandler]Β Forward \Throwable (fancyweb) + * feature #33574 [Http][DI] Replace REMOTE_ADDR in trusted proxies with the current REMOTE_ADDR (mcfedr) + * feature #33113 [Messenger][DX] Display real handler if handler is wrapped (DavidBadura) + * feature #33128 [FrameworkBundle] Sort tagged services (krome162504) + * feature #33658 [Yaml] fix parsing inline YAML spanning multiple lines (xabbuh) + * feature #33698 [HttpKernel] compress files generated by the profiler (nicolas-grekas) + * feature #33317 [Messenger] Added support for `from_transport` attribute on `messenger.message_handler` tag (ruudk) + * feature #33584 [Security] Deprecate isGranted()/decide() on more than one attribute (wouterj) + * feature #33663 [Security] Make stateful firewalls turn responses private only when needed (nicolas-grekas) + * feature #33609 [Form][SubmitType] Add "validate" option (fancyweb) + * feature #33621 Revert "feature #33507 [WebProfiler] Deprecated intercept_redirects in 4.4 (dorumd)" (lyrixx) + * feature #33605 [Twig] Add NotificationEmail (fabpot) + * feature #33623 [DependencyInjection] Allow binding iterable and tagged services (lyrixx) + * feature #33507 [WebProfiler] Deprecated intercept_redirects in 4.4 (dorumd) + * feature #33579 Adding .gitattributes to remove Tests directory from "dist" (Nyholm) + * feature #33562 [Mailer] rename SmtpEnvelope to Envelope (xabbuh) + * feature #33565 [Mailer] Rename an exception class (fabpot) + * feature #33516 [Cache] Added reserved characters constant for CacheItem (andyexeter) + * feature #33503 [SecurityBundle] Move Anonymous DI integration to new AnonymousFactory (wouterj) + * feature #33535 [WebProfilerBundle] Assign automatic colors to custom Stopwatch categories (javiereguiluz) + * feature #32565 [HttpClient] Allow enabling buffering conditionally with a Closure (rjwebdev) + * feature #32032 [DI] generate preload.php file for PHP 7.4 in cache folder (nicolas-grekas) + * feature #33117 [FrameworkBundle] Added --sort option for TranslationUpdateCommand (k0d3r1s) + * feature #32832 [Serializer] Allow multi-dimenstion object array in AbstractObjectNormalizer (alediator) + * feature #33189 New welcome page on startup for 4.4 LTS & 5.0 (yceruto) + * feature #33295 [OptionsResolver] Display full nested option hierarchy in exceptions (fancyweb) + * feature #33486 [VarDumper] Display fully qualified title (pavinthan, nicolas-grekas) + * feature #33496 Deprecated not passing dash symbol (-) to STDIN commands (yceruto) + * feature #32742 [Console] Added support for definition list and horizontal table (lyrixx) + * feature #33494 [Mailer] Change DSN syntax (fabpot) + * feature #33471 [Mailer] Check email validity before opening an SMTP connection (fabpot) + * feature #31177 #21571 Comparing roles to detected that users has changed (oleg-andreyev) + * feature #33459 [Validator] Deprecated CacheInterface in favor of PSR-6 (derrabus) + * feature #33271 Added new ErrorController + Preview and enabling there the error renderer mechanism (yceruto) + * feature #33454 [Mailer] Improve an exception when trying to send a RawMessage without an Envelope (fabpot) + * feature #33327 [ErrorHandler] Registering basic exception handler for late failures (yceruto) + * feature #33446 [TwigBridge] lint all templates from configured Twig paths if no argument was provided (yceruto) + * feature #33409 [Mailer] Add support for multiple mailers (fabpot) + * feature #33424 [Mailer] Change the DSN semantics (fabpot) + * feature #33319 Allow configuring class names through methods instead of class parameters in Doctrine extensions (alcaeus) + * feature #33283 [ErrorHandler] make DebugClassLoader able to add return type declarations (nicolas-grekas) + * feature #33323 [TwigBridge] Throw an exception when one uses email as a context variable in a TemplatedEmail (fabpot) + * feature #33308 [SecurityGuard] Deprecate returning non-boolean values from checkCredentials() (derrabus) + * feature #33217 [FrameworkBundle][DX] Improving the redirect config when using RedirectController (yceruto) + * feature #33015 [HttpClient] Added TraceableHttpClient and WebProfiler panel (jeremyFreeAgent) + * feature #33091 [Mime] Add Address::fromString (gisostallenberg) + * feature #33144 [DomCrawler] Added Crawler::matches(), ::closest(), ::outerHtml() (lyrixx) + * feature #33152 Mark all dispatched event classes as final (Tobion) + * feature #33258 [HttpKernel] deprecate global dir to load resources from (Tobion) + * feature #33272 [Translation] deprecate support for null locales (xabbuh) + * feature #33269 [TwigBridge] Mark all classes extending twig as @final (fabpot) + * feature #33270 [Mime] Remove NamedAddress (fabpot) + * feature #33169 [HttpFoundation] Precalculate session expiry timestamp (azjezz) + * feature #33237 [Mailer] Remove the auth mode DSN option and support in the eSMTP transport (fabpot) + * feature #33233 [Mailer] Simplify the way TLS/SSL/STARTTLS work (fabpot) + * feature #32360 [Monolog] Added ElasticsearchLogstashHandler (lyrixx) + * feature #32489 [Messenger] Allow exchange type headers binding (CedrickOka) + * feature #32783 [Messenger] InMemoryTransport handle acknowledged and rejected messages (tienvx) + * feature #33155 [ErrorHandler] Added call() method utility to turns any PHP error into \ErrorException (yceruto) + * feature #33203 [Mailer] Add support for the queued flag in the EmailCount assertion (fabpot) + * feature #30323 [ErrorHandler] trigger deprecation in DebugClassLoader when child class misses a return type (fancyweb, nicolas-grekas) + * feature #33137 [DI] deprecate support for non-object services (nicolas-grekas) + * feature #32845 [HttpKernel][FrameworkBundle] Add alternative convention for bundle directories (yceruto) + * feature #32548 [Translation] XliffLintCommand: allow .xliff file extension (codegain) + * feature #28363 [Serializer] Encode empty objects as objects, not arrays (mcfedr) + * feature #33122 [WebLink] implement PSR-13 directly (nicolas-grekas) + * feature #33078 Add compatibility trait for PHPUnit constraint classes (alcaeus) + * feature #32988 [Intl] Support ISO 3166-1 Alpha-3 country codes (terjebraten-certua) + * feature #32598 [FrameworkBundle][Routing] Private service route loaders (fancyweb) + * feature #32486 [DoctrineBridge] Invokable event listeners (fancyweb) + * feature #31083 [Validator] Allow objects implementing __toString() to be used as violation messages (mdlutz24) + * feature #32122 [HttpFoundation] deprecate HeaderBag::get() returning an array and add all($key) instead (Simperfit) + * feature #32807 [HttpClient] add "max_duration" option (fancyweb) + * feature #31546 [Dotenv] Use default value when referenced variable is not set (j92) + * feature #32930 [Mailer][Mime] Add PHPUnit constraints and assertions for the Mailer (fabpot) + * feature #32912 [Mailer] Add support for the profiler (fabpot) + * feature #32940 [PhpUnitBridge] Add polyfill for PhpUnit namespace (jderusse) + * feature #31843 [Security] add support for opportunistic password migrations (nicolas-grekas) + * feature #32824 [Ldap] Add security LdapUser and provider (chalasr) + * feature #32922 [PhpUnitBridge] make the bridge act as a polyfill for newest PHPUnit features (nicolas-grekas) + * feature #32927 [Mailer] Add message events logger (fabpot) + * feature #32916 [Mailer] Add a name to the transports (fabpot) + * feature #32917 [Mime] Add AbstractPart::asDebugString() (fabpot) + * feature #32543 [FrameworkBundle] add config translator cache_dir (Raulnet) + * feature #32669 [Yaml] Add flag to dump NULL as ~ (OskarStark) + * feature #32896 [Mailer] added debug info to TransportExceptionInterface (fabpot) + * feature #32817 [DoctrineBridge] Deprecate RegistryInterface (Koc) + * feature #32504 [ErrorRenderer] Add DebugCommand for easy debugging and testing (yceruto) + * feature #32581 [DI] Allow dumping the container in one file instead of many files (nicolas-grekas) + * feature #32762 [Form][DX] derive default timezone from reference_date option when possible (yceruto) + * feature #32745 [Messenger][Profiler] Attempt to give more useful source info when using HandleTrait (ogizanagi) + * feature #32680 [Messenger][Profiler] Collect the stamps at the end of dispatch (ogizanagi) + * feature #32683 [VarDumper] added support for Imagine/Image (lyrixx) + * feature #32749 [Mailer] Make transport factory test case public (Koc) + * feature #32718 [Form] use a reference date to handle times during DST (xabbuh) + * feature #32637 [ErrorHandler] Decouple from ErrorRenderer component (yceruto) + * feature #32609 [Mailer][DX][RFC] Rename mailer bridge transport classes (Koc) + * feature #32587 [Form][Validator] Generate accept attribute with file constraint and mime types option (Coosos) + * feature #32658 [Form] repeat preferred choices in list of all choices (Seb33300, xabbuh) + * feature #32698 [WebProfilerBundle] mark all classes as internal (Tobion) + * feature #32695 [WebProfilerBundle] Decoupling TwigBundle and using the new ErrorRenderer mechanism (yceruto) + * feature #31398 [TwigBundle] Deprecating error templates for non-html formats and using ErrorRenderer as fallback (yceruto) + * feature #32582 [Routing] Deprecate ServiceRouterLoader and ObjectRouteLoader in favor of ContainerLoader and ObjectLoader (fancyweb) + * feature #32661 [ErrorRenderer] Improving the exception page provided by HtmlErrorRenderer (yceruto) + * feature #32332 [DI] Move non removing compiler passes to after removing passes (alexpott) + * feature #32475 [Process] Deprecate Process::inheritEnvironmentVariables() (ogizanagi) + * feature #32583 [Mailer] Logger vs debug mailer (fabpot) + * feature #32471 Add a new ErrorHandler component (mirror of the Debug component) (yceruto) + * feature #32463 [VarDumper] Allow to configure VarDumperTestTrait casters & flags (ogizanagi) + * feature #31946 [Mailer] Extract transport factory and allow create custom transports (Koc) + * feature #31194 [PropertyAccess] Improve errors when trying to find a writable property (pierredup) + * feature #32435 [Validator] Add a new constraint message when there is both min and max (Lctrs) + * feature #32470 Rename ErrorCatcher to ErrorRenderer (rendering part only) (yceruto) + * feature #32462 [WebProfilerBundle] Deprecating templateExists method (yceruto) + * feature #32446 [Lock] rename and deprecate Factory into LockFactory (Simperfit) + * feature #31975 Dynamic bundle assets (garak) + * feature #32429 [VarDumper] Let browsers trigger their own search on double CMD/CTRL + F (ogizanagi) + * feature #32198 [Lock] Split "StoreInterface" into multiple interfaces with less responsability (Simperfit) + * feature #31511 [Validator] Allow to use property paths to get limits in range constraint (Lctrs) + * feature #32424 [Console] don't redraw progress bar more than every 100ms by default (nicolas-grekas) + * feature #32418 [Console] Added Application::reset() (lyrixx) + * feature #31217 [WebserverBundle] Deprecate the bundle in favor of symfony local server (Simperfit) + * feature #31554 [SECURITY] AbstractAuthenticationListener.php error instead info. Rebase of #28462 (berezuev) + * feature #32284 [Cache] Add argument $prefix to AdapterInterface::clear() (nicolas-grekas) + * feature #32423 [ServerBundle] Display all logs by default (lyrixx) + * feature #26339 [Console] Add ProgressBar::preventRedrawFasterThan() and forceRedrawSlowerThan() methods (ostrolucky) + * feature #31269 [Translator] Dump native plural formats to po files (Stadly) + * feature #31560 [Ldap][Security] LdapBindAuthenticationProvider does not bind before search query (Simperfit) + * feature #31626 [Console] allow answer to be trimmed by adding a flag (Simperfit) + * feature #31876 [WebProfilerBundle] Add clear button to ajax tab (Matts) + * feature #32415 [Translation] deprecate passing a null locale (Simperfit) + * feature #32290 [HttpClient] Add $response->toStream() to cast responses to regular PHP streams (nicolas-grekas) + * feature #32402 [Intl] Exclude root language (ro0NL) + * feature #32295 [FrameworkBundle] Add autowiring alias for PSR-14 (nicolas-grekas) + * feature #32390 [DependencyInjection] Deprecated passing Parameter instances as class name to Definition (derrabus) + * feature #32106 [FrameworkBundle] Use default_locale as default value for translator.fallbacks (dunglas) + * feature #32294 [FrameworkBundle] Allow creating chained cache pools by providing several adapters (nicolas-grekas) + * feature #32207 [FrameworkBundle] Allow to use the BrowserKit assertions with Panther and API Platform's test client (dunglas) + * feature #32344 [HttpFoundation][HttpKernel] Improving the request/response format autodetection (yceruto) + * feature #32231 [HttpClient] Add support for NTLM authentication (nicolas-grekas) + * feature #32265 [Validator] deprecate non-string constraint violation codes (xabbuh) + * feature #31528 [Validator] Add a Length::$allowEmptyString option to reject empty strings (ogizanagi) + * feature #32081 [WIP][Mailer] Overwrite envelope sender and recipients from config (Devristo) + * feature #32255 [HttpFoundation] Drop support for ApacheRequest (lyrixx) + * feature #31825 [Messenger] Added support for auto trimming of redis streams (Toflar) + * feature #32277 Remove @experimental annotations (fabpot) + * feature #30981 [Mime] S/MIME Support (sstok) + * feature #32180 [Lock] add an InvalidTTLException to be more accurate (Simperfit) + * feature #32241 [PropertyAccess] Deprecate null as allowed value for defaultLifetime argument in createCache method (jschaedl) + * feature #32221 [ErrorCatcher] Make IDEs and static analysis tools happy (fabpot) + * feature #32227 Rename the ErrorHandler component to ErrorCatcher (fabpot) + * feature #31065 Add ErrorHandler component (yceruto) + * feature #32126 [Process] Allow writing portable "prepared" command lines (Simperfit) + * feature #31532 [Ldap] Add users extraFields in ldap component (Simperfit) + * feature #32104 Add autowiring for HTTPlug (nicolas-grekas) + * feature #32130 [Form] deprecate int/float for string input in NumberType (xabbuh) + * feature #31547 [Ldap] Add exception for mapping ldap errors (Simperfit) + * feature #31764 [FrameworkBundle] add attribute stamps (walidboughdiri) + * feature #32059 [PhpUnitBridge] Bump PHPUnit 7+8 (ro0NL) + * feature #32041 [Validator] Deprecate unused arg in ExpressionValidator (ogizanagi) + * feature #31287 [Config] Introduce find method in ArrayNodeDefinition to ease configuration tree manipulation (jschaedl) + * feature #31959 [DomCrawler][Feature][DX] Add Form::getName() method (JustBlackBird) + * feature #32026 [VarDumper] caster for HttpClient's response dumps all info (nicolas-grekas) + * feature #31976 [HttpClient] add HttplugClient for compat with libs that need httplug v1 or v2 (nicolas-grekas) + * feature #31956 [Mailer] Changed EventDispatcherInterface dependency from Component to Contracts (Koc) + * feature #31980 [HttpClient] make Psr18Client implement relevant PSR-17 factories (nicolas-grekas) + * feature #31919 [WebProfilerBundle] Select default theme based on user preferences (javiereguiluz) + * feature #31451 [FrameworkBundle] Allow dots in translation domains (jschaedl) + * feature #31321 [DI] deprecates tag !tagged in favor of !tagged_iterator (jschaedl) + * feature #31658 [HTTP Foundation] Deprecate passing argument to method Request::isMethodSafe() (dFayet) + * feature #31597 [Security] add MigratingPasswordEncoder (nicolas-grekas) + * feature #31351 [Validator] Improve TypeValidator to handle array of types (jschaedl) + * feature #31526 [Validator] Add compared value path to violation parameters (ogizanagi) + * feature #31514 Add exception as HTML comment to beginning and end of `exception_full.html.twig` (ruudk) + * feature #31739 [FrameworkBundle] Add missing BC layer for deprecated ControllerNameParser injections (chalasr) + * feature #31831 [HttpClient] add $response->cancel() (nicolas-grekas) + * feature #31334 [Messenger] Add clear Entity Manager middleware (Koc) + * feature #31594 [Security] add PasswordEncoderInterface::needsRehash() (nicolas-grekas) + * feature #31821 [FrameworkBundle][TwigBundle] Add missing deprecations for PHP templating layer (yceruto) + * feature #31509 [Monolog] Setup the LoggerProcessor after all other processor (lyrixx) + * feature #31785 [Messenger] Deprecate passing a bus locator to ConsumeMessagesCommand's constructor (chalasr) + * feature #31700 [MonologBridge] RouteProcessor class is now final to ease the the removal of deprecated event (Simperfit) + * feature #31732 [HttpKernel] Make DebugHandlersListener internal (chalasr) + * feature #31539 [HttpKernel] Add lts config (noniagriconomie) + * feature #31437 [Cache] Add Redis Sentinel support (StephenClouse) + * feature #31543 [DI] deprecate short callables in yaml (nicolas-grekas) + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index d211dd419d064..c0b3daf15d463 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,83 +1,8 @@ -Code of Conduct -=============== +# Code of Conduct -Our Pledge ----------- +This project follows a [Code of Conduct][code_of_conduct] in order to ensure an open and welcoming environment. +Please read the full text for understanding the accepted and unaccepted behavior. +Please read also the [reporting guidelines][guidelines], in case you encountered or witnessed any misbehavior. -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnic origin, gender identity and expression, level of -experience, education, socio-economic status, nationality, personal appearance, -religion, or sexual identity and orientation. - -Our Standards -------------- - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -Our Responsibilities --------------------- - -[CoC Active Response Ensurers, or CARE][1], are responsible for clarifying the -standards of acceptable behavior and are expected to take appropriate and fair -corrective action in response to any instances of unacceptable behavior. - -CARE team members have the right and responsibility to remove, edit, or reject -comments, commits, code, wiki edits, issues, and other contributions that are -not aligned to this Code of Conduct, or to ban temporarily or permanently any -contributor for other behaviors that they deem inappropriate, threatening, -offensive, or harmful. - -Scope ------ - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by CARE team members. - -Enforcement ------------ - -Instances of abusive, harassing, or otherwise unacceptable behavior -[may be reported][2] by contacting the [CARE team members][1]. -All complaints will be reviewed and investigated and will result in a response -that is deemed necessary and appropriate to the circumstances. The CARE team is -obligated to maintain confidentiality with regard to the reporter of an -incident. Further details of specific enforcement policies may be posted -separately. - -CARE team members who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by the -[core team][3]. - -Attribution ------------ - -This Code of Conduct is adapted from the [Contributor Covenant version 1.4][4]. - -[1]: https://symfony.com/doc/current/contributing/code_of_conduct/care_team.html -[2]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html -[3]: https://symfony.com/doc/current/contributing/code/core_team.html -[4]: https://www.contributor-covenant.org/version/1/4/code-of-conduct.html +[code_of_conduct]: https://symfony.com/coc +[guidelines]: https://symfony.com/doc/current/contributing/code_of_conduct/reporting_guidelines.html diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 011ad9bee0777..b136e74da6266 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -1,1090 +1,2081 @@ CONTRIBUTORS ============ -Symfony is the result of the work of many people who made the code better -(see https://symfony.com/contributors for more information): +Symfony is the result of the work of many people who made the code better. +The Symfony Connect username in parenthesis allows to get more information - Fabien Potencier (fabpot) - Nicolas Grekas (nicolas-grekas) - - Bernhard Schussek (bschussek) + - Alexander M. Turek (derrabus) - Christian Flothmann (xabbuh) + - Robin Chalas (chalas_r) + - Bernhard Schussek (bschussek) - Tobias Schultze (tobion) + - Thomas Calvet (fancyweb) + - JΓ©rΓ©my DERUSSΓ‰ (jderusse) + - GrΓ©goire Pineau (lyrixx) + - Wouter de Jong (wouterj) + - Maxime Steinhausser (ogizanagi) + - KΓ©vin Dunglas (dunglas) - Christophe Coevoet (stof) - - Robin Chalas (chalas_r) - Jordi Boggiano (seldaek) + - Roland Franssen (ro0) - Victor Berchet (victor) - - KΓ©vin Dunglas (dunglas) - - Maxime Steinhausser (ogizanagi) + - Yonel Ceruto (yonelceruto) + - Tobias Nyholm (tobias) + - Javier Eguiluz (javier.eguiluz) + - Oskar Stark (oskarstark) - Ryan Weaver (weaverryan) - - Jakub Zalas (jakubzalas) - Johannes S (johannes) - - Javier Eguiluz (javier.eguiluz) - - Roland Franssen (ro0) + - Jakub Zalas (jakubzalas) - Kris Wallsmith (kriswallsmith) - - GrΓ©goire Pineau (lyrixx) - Hugo Hamon (hhamon) - - Abdellatif Ait boudad (aitboudad) + - Hamza Amrouche (simperfit) - Samuel ROZE (sroze) - - Romain Neutron (romain) - Pascal Borreli (pborreli) - - Wouter De Jong (wouterj) + - Romain Neutron + - Jules Pietri (heah) - Joseph Bielawski (stloyd) - - Karma Dordrak (drak) + - Drak (drak) + - Abdellatif Ait boudad (aitboudad) - Lukas Kahwe Smith (lsmith) - - Yonel Ceruto (yonelceruto) + - Jan SchΓ€dlich (jschaedl) - Martin Hasoň (hason) + - JΓ©rΓ΄me Tamarelle (gromnan) - Jeremy Mikola (jmikola) + - Kevin Bond (kbond) - Jean-FranΓ§ois Simon (jfsimon) - - Jules Pietri (heah) - Benjamin Eberlei (beberlei) - - Igor Wiedler (igorw) - - Eriksen Costa (eriksencosta) - - Hamza Amrouche (simperfit) - - Guilhem Niot (energetick) - - Sarah Khalil (saro0h) + - Igor Wiedler + - HypeMC (hypemc) + - Valentin Udaltsov (vudaltsov) + - Vasilij DuΕ‘ko (staff) + - Matthias Pigulla (mpdude) + - Laurent VOULLEMIER (lvo) + - Antoine Makdessi (amakdessi) + - Pierre du Plessis (pierredup) + - GrΓ©goire Paris (greg0ire) + - Gabriel OstroluckΓ½ (gadelat) - Jonathan Wage (jwage) - - Tobias Nyholm (tobias) - - Lynn van der Berg (kjarli) - - JΓ©rΓ©my DERUSSΓ‰ (jderusse) - - Diego Saint Esteben (dosten) + - David Maicher (dmaicher) + - Titouan Galopin (tgalopin) - Alexandre SalomΓ© (alexandresalome) - - William Durand (couac) + - William DURAND + - Alexander Schranz (alexander-schranz) - ornicar - - Alexander M. Turek (derrabus) - Dany Maillard (maidmaid) - - Francis Besset (francisbesset) + - Mathieu Santostefano (welcomattic) + - Eriksen Costa + - Diego Saint Esteben (dosten) - stealth35 ‏ (stealth35) - Alexander Mols (asm89) - - Matthias Pigulla (mpdude) + - GΓ‘bor Egyed (1ed) + - Francis Besset (francisbesset) + - Alexandre Daubois (alexandre-daubois) + - Vasilij Dusko | CREATION - Bulat Shakirzyanov (avalanche123) + - Iltar van der Berg + - Miha Vrhovnik (mvrhov) + - Mathieu Piot (mpiot) - SaΕ‘a StamenkoviΔ‡ (umpirsky) - - Peter Rehm (rpet) - - Pierre du Plessis (pierredup) - - Kevin Bond (kbond) - - Henrik BjΓΈrnskov (henrikbjorn) - - Miha Vrhovnik - - Diego Saint Esteben (dii3g0) - - GrΓ©goire Paris (greg0ire) + - Alex Pott + - Guilhem N (guilhemn) + - Vincent Langlet (deviling) + - Vladimir Reznichenko (kalessil) + - Sarah Khalil (saro0h) - Konstantin Kudryashov (everzet) - - GΓ‘bor Egyed (1ed) - - Titouan Galopin (tgalopin) - - Konstantin Myakshin (koc) + - Tomas NorkΕ«nas (norkunas) - Bilal Amarni (bamarni) - - Mathieu Piot (mpiot) - - David Maicher (dmaicher) + - Eriksen Costa - Florin Patan (florinpatan) - - Valentin Udaltsov (vudaltsov) - - Gabriel OstroluckΓ½ (gadelat) - - Vladimir Reznichenko (kalessil) + - Peter Rehm (rpet) + - Mathieu Lechat (mat_the_cat) + - Henrik BjΓΈrnskov (henrikbjorn) + - David Buchmann (dbu) + - Konstantin Myakshin (koc) + - Andrej Hudec (pulzarraider) + - Julien Falque (julienfalque) + - Massimiliano Arione (garak) + - Douglas Greenshields (shieldo) + - Christian Raue - JΓ‘chym TouΕ‘ek (enumag) + - Mathias Arlaud (mtarld) + - Graham Campbell (graham) - Michel Weimerskirch (mweimerskirch) - - Andrej Hudec (pulzarraider) - - Issei Murasawa (issei_m) - Eric Clemmons (ericclemmons) - - Charles Sarrazin (csarrazi) - - Christian Raue + - Issei Murasawa (issei_m) + - Fran Moreno (franmomu) + - Malte SchlΓΌter (maltemaltesich) + - Antoine Lamirault + - Vasilij Dusko + - Denis (yethee) - Arnout Boks (aboks) - - Deni + - Charles Sarrazin (csarrazi) + - PrzemysΕ‚aw Bogusz (przemyslaw-bogusz) - Henrik Westphal (snc) - Dariusz GΓ³recki (canni) - - Douglas Greenshields (shieldo) - - David Buchmann (dbu) - - Dariusz Ruminski + - Maxime Helias (maxhelias) + - Ener-Getick + - Ruud Kamphuis (ruudk) + - Sebastiaan Stok (sstok) + - JΓ©rΓ΄me Vasseur (jvasseur) + - Ion Bazan (ionbazan) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - - Graham Campbell (graham) - Daniel Holmes (dholmes) - - Thomas Calvet (fancyweb) - Toni Uebernickel (havvg) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) - - JΓ©rΓ΄me Tamarelle (gromnan) + - Smaine Milianni (ismail1432) - John Wards (johnwards) - - Fran Moreno (franmomu) + - Dariusz Ruminski + - Lars Strojny (lstrojny) + - Yanick Witschi (toflar) - Antoine HΓ©rault (herzult) - - ParΓ‘da JΓ³zsef (paradajozsef) + - Konstantin.Myakshin + - Rokas MikalkΔ—nas (rokasm) + - Arman Hosseini (arman) - Arnaud Le Blanc (arnaud-lb) - Maxime STEINHAUSSER - - Michal Piotrowski (eventhorizon) + - Peter Kokot (maastermedia) + - Saif Eddin Gmati (azjezz) + - Ahmed TAILOULOUTE (ahmedtai) + - Simon Berger - Tim Nagel (merk) + - Andreas Braun + - Teoh Han Hui (teohhanhui) + - YaFou + - Gary PEGEOT (gary-p) - Chris Wilkinson (thewilkybarkid) - Brice BERNARD (brikou) + - Roman Martinuk (a2a4) + - Gregor Harlan (gharlan) - Baptiste ClaviΓ© (talus) + - Adrien Brault (adrienbrault) + - Michal Piotrowski - marc.weistroff - - TomΓ‘Ε‘ Votruba (tomas_votruba) - lenar - - Alexander Schwenn (xelaris) + - Jesse Rushlow (geeshoe) + - ThΓ©o FIDRY + - jeremyFreeAgent (jeremyfreeagent) + - Jeroen Spee (jeroens) + - Michael Babker (mbabker) - WΕ‚odzimierz Gajda (gajdaw) - - JΓ©rΓ΄me Vasseur (jvasseur) - - Peter Kokot (maastermedia) + - Christian Scheb + - Guillaume (guill) + - Tugdual Saunier (tucksaun) - Jacob Dreesen (jdreesen) + - Joel Wurtz (brouznouf) + - Olivier Dolbeau (odolbeau) - Florian Voutzinos (florianv) - - Jan SchΓ€dlich (jschaedl) + - zairig imad (zairigimad) - Colin Frei + - Christopher Hertel (chertel) - Javier Spagnoletti (phansys) - - Adrien Brault (adrienbrault) - - Joshua Thijssen - - Daniel Wehner (dawehner) - excelwebzone - - Gordon Franke (gimler) - - Sebastiaan Stok (sstok) + - Phil Taylor (prazgod) + - JΓ©rΓ΄me Parmentier (lctrs) + - HeahDude + - Richard van Laak (rvanlaak) + - ParΓ‘da JΓ³zsef (paradajozsef) + - Alessandro Lai (jean85) + - Alexander Schwenn (xelaris) - Fabien Pennequin (fabienpennequin) - - ThΓ©o FIDRY (theofidry) - - Eric GELOEN (gelo) - - Joel Wurtz (brouznouf) - - Lars Strojny (lstrojny) - - Tugdual Saunier (tucksaun) + - Gordon Franke (gimler) + - FranΓ§ois-Xavier de Guillebon (de-gui_f) + - Andreas Schempp (aschempp) + - Gabriel Caruso + - Anthony GRASSIOT (antograssiot) + - Jan Rosier (rosier) + - Daniel Wehner (dawehner) + - Hugo Monteiro (monteiro) + - Baptiste Leduc (korbeil) + - Marco Pivetta (ocramius) - Robert SchΓΆnthal (digitalkaoz) - - Florian Lonqueu-Brochard (florianlb) - - Oskar Stark (oskarstark) + - Hugo Alliaume (kocal) + - VΓ΅ XuΓ’n TiαΊΏn (tienvx) + - fd6130 (fdtvui) + - Tigran Azatyan (tigranazatyan) + - Eric GELOEN (gelo) + - Matthieu Napoli (mnapoli) + - TomΓ‘Ε‘ Votruba (tomas_votruba) + - Joshua Thijssen - Stefano Sala (stefano.sala) - - Evgeniy (ewgraf) - - Alex Pott - - Vincent AUBERT (vincent) + - Alessandro Chitolina (alekitto) + - Valentine Boineau (valentineboineau) + - Jeroen Noten (jeroennoten) + - Gocha Ossinkine (ossinkine) + - Andreas MΓΆller (localheinz) + - OGAWA Katsuhiro (fivestar) + - Jhonny Lidfors (jhonne) + - Martin Hujer (martinhujer) + - Wouter J + - Chi-teck + - Guilliam Xavier + - Antonio Pauletich (x-coder264) + - Timo Bakx (timobakx) - Juti Noppornpitak (shiroyuki) - - Teoh Han Hui (teohhanhui) - - Anthony MARTIN (xurudragon) - - Tigran Azatyan (tigranazatyan) + - Joe Bennett (kralos) + - Nate Wiebe (natewiebe13) + - Anthony MARTIN + - Colin O'Dell (colinodell) - Sebastian HΓΆrl (blogsh) + - Ben Davies (bendavies) - Daniel Gomes (danielcsgomes) - - Gabriel Caruso + - Michael KΓ€fer (michael_kaefer) - Hidenori Goto (hidenorigoto) + - Dāvis ZālΔ«tis (k0d3r1s) + - Albert Casademont (acasademont) - Arnaud Kleinpeter (nanocom) - - Jannik Zschiesche (apfelbox) - Guilherme Blanco (guilhermeblanco) + - Michael VoΕ™Γ­Ε‘ek + - Farhad Safarov (safarov) - SpacePossum - Pablo Godel (pgodel) - - JΓ©rΓ©mie Augustin (jaugustin) - - Oleg Voronkovich + - Denis Brumann (dbrumann) + - Romaric Drigon (romaricdrigon) - AndrΓ©ia Bohner (andreia) - - Philipp Wahala (hifi) - - Julien Falque (julienfalque) + - Jannik Zschiesche - Rafael Dohms (rdohms) + - George Mponos (gmponos) + - Fritz Michael Gschwantner (fritzmg) + - Aleksandar Jakovljevic (ajakov) - jwdeitch - - Mikael Pajunen - - FranΓ§ois-Xavier de Guillebon (de-gui_f) + - Juri 8000 ca Vlahoviček (vjurica) + - David PrΓ©vot + - Vincent Touzet (vincenttouzet) + - Fabien Bourigault (fbourigault) + - JΓ©rΓ©my DerussΓ© + - Nicolas Philippe (nikophil) + - Hubert Lenoir (hubert_lenoir) + - Florent Mata (fmata) + - mcfedr (mcfedr) + - Maciej Malarz (malarzm) + - Soner Sayakci + - Artem Lopata + - Sokolov Evgeniy (ewgraf) + - Stadly + - Justin Hileman (bobthecow) - Niels Keurentjes (curry684) - Vyacheslav Pavlov - - Richard van Laak (rvanlaak) - Richard Shank (iampersistent) - - Thomas Rabaix (rande) + - Andre RΓΈmcke (andrerom) + - Dmitrii Poddubnyi (karser) + - soyuka + - Sergey (upyx) - Rouven Weßling (realityking) + - BoShurik + - Zmey - Clemens Tolboom + - Oleg Voronkovich - Helmer Aaviksoo - - Alessandro Chitolina (alekitto) - - Hiromi Hishida (77web) + - MichaΕ‚ (bambucha15) + - Remon van de Kamp + - Ben Hakim + - Sylvain Fabre (sylfabre) + - Filippo Tessarotto (slamdunk) + - Tom Van Looy (tvlooy) + - 77web + - Bohan Yang (brentybh) + - Bastien Jaillot (bastnic) + - W0rma - Matthieu Ouellette-Vachon (maoueh) - - Massimiliano Arione (garak) + - Lynn van der Berg (kjarli) - MichaΕ‚ Pipa (michal.pipa) - Dawid Nowak - - George Mponos (gmponos) - Amal Raghav (kertz) - - Jonathan Ingram (jonathaningram) + - Jonathan Ingram - Artur Kotyrba - Tyson Andre + - Thomas Landauer (thomas-landauer) - GDIBass - Samuel NELA (snela) - - Vincent Touzet (vincenttouzet) - - Alexander Schranz (alexander-schranz) - - jeremyFreeAgent (JΓ©rΓ©my Romey) (jeremyfreeagent) + - dFayet + - Karoly Gossler (connorhu) + - Vincent AUBERT (vincent) + - Sebastien Morel (plopix) + - Yoann RENARD (yrenard) + - Thomas Lallement (raziel057) + - TimothΓ©e Barray (tyx) - James Halsall (jaitsu) - - Matthieu Napoli (mnapoli) - - Florent Mata (fmata) + - Mikael Pajunen - Warnar Boekkooi (boekkooi) + - Marco Petersen (ocrampete16) + - Benjamin Leveque (benji07) - Dmitrii Chekaliuk (lazyhammer) - ClΓ©ment JOBEILI (dator) + - Vilius GrigaliΕ«nas - Marek Ε tΓ­pek (maryo) + - Patrick Landolt (scube) + - FranΓ§ois Pluchino (francoispluchino) - Daniel Espendiller - - Possum + - Arnaud PETITPAS (apetitpa) - Dorian Villet (gnutix) + - Wojciech Kania + - Alexey Kopytko (sanmai) - Sergey Linnik (linniksa) - - Richard Miller (mr_r_miller) - - Albert Casademont (acasademont) + - Warxcell (warxcell) + - Richard Miller + - Leo Feyer (leofeyer) - Mario A. Alvarez Garcia (nomack84) - - Dennis Benkert (denderello) + - Thomas Rabaix (rande) + - D (denderello) + - Jonathan Scheiber (jmsche) - DQNEO - - Gregor Harlan (gharlan) - - Gary PEGEOT (gary-p) + - Andrii Bodnar + - Artem (artemgenvald) + - ivan + - Sergey Belyshkin (sbelyshkin) + - Urinbayev Shakhobiddin (shokhaa) + - Ahmed Raafat + - Philippe Segatori + - Thibaut Cheymol (tcheymol) + - Julien Pauli + - Islam Israfilov (islam93) + - Oleg Andreyev (oleg.andreyev) + - Daniel Gorgan + - SΓ©bastien Alfaiate (seb33300) + - Hendrik Luup (hluup) + - Martin Herndl (herndlm) - Ruben Gonzalez (rubenrua) - Benjamin Dulau (dbenjamin) + - Pavel Kirpitsov (pavel-kirpichyov) - Mathieu Lemoine (lemoinem) - Christian Schmidt - Andreas Hucks (meandmymonkey) - - Tom Van Looy (tvlooy) - Noel Guilbert (noel) - - Yanick Witschi (toflar) + - Hamza Makraz (makraz) + - Loick Piera (pyrech) + - Vitalii Ekert (comrade42) + - Clara van Miert + - Martin Auswöger + - Alexander Menshchikov - Stepan Anchugov (kix) - bronze1man - sun (sun) + - Alan Poulain (alanpoulain) - Larry Garfield (crell) - - MichaΓ«l Perrin (michael.perrin) + - Fabien Villepinte + - SiD (plbsid) + - Thomas Bisignani (toma) + - Edi ModriΔ‡ (emodric) + - Philipp Wahala (hifi) - Nikolay Labinskiy (e-moe) - Martin Schuhfuß (usefulthink) - apetitpa - - Matthieu Bontemps (mbontemps) - - apetitpa + - Vladyslav Loboda - Pierre Minnieur (pminnieur) - - fivestar + - Kyle - Dominique Bongiraud - - Jeremy Livingston (jeremylivingston) - - Michael Lee (zerustech) - - Matthieu Auger (matthieuauger) + - Hidde Wieringa (hiddewie) + - Christopher Davis (chrisguitarguy) + - LukΓ‘Ε‘ Holeczy (holicz) + - Florian Lonqueu-Brochard (florianlb) - Leszek Prabucki (l3l0) - - Fabien Bourigault (fbourigault) + - Emanuele Panzeri (thepanz) + - Matthew Smeets - FranΓ§ois Zaninotto (fzaninotto) + - Alexis Lefebvre - Dustin Whittle (dustinwhittle) - jeff - John Kary (johnkary) - - Andreas Schempp (aschempp) - - Justin Hileman (bobthecow) - - Blanchon Vincent (blanchonvincent) + - Bob van de Vijver (bobvandevijver) + - smoench - Michele Orselli (orso) - Sven Paulus (subsven) + - Daniel STANCU + - Markus Fasselt (digilist) - Maxime Veber (nek-) + - Marcin SikoΕ„ (marphi) + - Sullivan SENECHAL (soullivaneuh) - Rui Marinho (ruimarinho) - - Eugene Wissner + - Marc Weistroff (futurecat) + - Dimitri Gritsajuk (ottaviano) + - Possum + - JΓ©rΓ©mie Augustin (jaugustin) - Pascal Montoya - - Julien Brochet (mewt) - - Leo Feyer - - Tristan Darricau (nicofuma) + - Julien Brochet + - MichaΓ«l Perrin (michael.perrin) + - Tristan Darricau (tristandsensio) + - Fabien S (bafs) - Victor Bocharsky (bocharsky_bw) + - Jan Sorgalla (jsor) + - henrikbjorn + - Alex Bowers + - Simon Podlipsky (simpod) - Marcel Beerta (mazen) - - Pavel Batanov (scaytrase) + - flack (flack) + - Craig Duncan (duncan3dc) - Mantis Development - - LoΓ―c Faugeron - - Hidde Wieringa (hiddewie) - - Andre RΓΈmcke (andrerom) - - Marco Pivetta (ocramius) + - Pablo Lozano (arkadis) + - Romain Monteil (ker0x) + - quentin neyrat (qneyrat) + - Antonio Jose Cerezo (ajcerezo) + - Marcin Szepczynski (czepol) + - Lescot Edouard (idetox) - Rob Frawley 2nd (robfrawley) - - julien pauli (jpauli) - - Lorenz Schori - - SΓ©bastien Lavoie (lavoiesl) + - Mohammad Emran Hasan (phpfour) + - Dmitriy Mamontov (mamontovdmitriy) + - Nikita Konstantinov (unkind) + - Michael Lee (zerustech) - Dariusz - - Michael Babker (mbabker) - Francois Zaninotto - - Alexander Kotynia (olden) + - Laurent MasfornΓ© (heisenberg) + - Claude Khedhiri (ck-developer) - Daniel Tschinder - Christian Schmidt - - Marcos SΓ‘nchez + - Alexander Kotynia (olden) + - Toni Rudolf (toooni) - Elnur Abdurrakhimov (elnur) + - Iker Ibarguren (ikerib) - Manuel Reinhard (sprain) - - Danny Berger (dpb587) - - Antonio J. GarcΓ­a Lagar (ajgarlag) + - Johann Pardanaud + - Indra Gunawan (indragunawan) + - Tim Goudriaan (codedmonkey) + - Harm van Tilborg (hvt) + - Baptiste Lafontaine (magnetik) + - Dries Vints - Adam Prager (padam87) - - PrzemysΕ‚aw Bogusz (przemyslaw-bogusz) + - JudicaΓ«l RUFFIEUX (axanagor) - BenoΓt Burnichon (bburnichon) - - Roman MarintΕ‘enko (inori) + - maxime.steinhausser + - simon chrzanowski (simonch) + - Andrew M-Y (andr) + - Krasimir Bosilkov (kbosilkov) + - Marcin Michalski (marcinmichalski) + - Roman Ring (inori) - Xavier MontaΓ±a Carreras (xmontana) - - RΓ©mon van de Kamp (rpkamp) - - MickaΓ«l Andrieu (mickaelandrieu) + - Tarmo LeppΓ€nen (tarlepp) + - AnneKir + - Tobias Weichart + - Miro Michalicka + - M. Vondano - Xavier Perez - Arjen Brouwer (arjenjb) - - Katsuhiro OGAWA + - Tavo Nieves J (tavoniievez) + - Arjen van der Meijden - Patrick McDougle (patrick-mcdougle) + - Jerzy (jlekowski) + - Danny Berger (dpb587) + - Marek Zajac - Alif Rachmawadi - Anton Chernikov (anton_ch1989) - - Kristen Gilden (kgilden) - - Pierre-Yves LEBECQ (pylebecq) + - Pierre-Yves Lebecq (pylebecq) + - Alireza Mirsepassi (alirezamirsepassi) - Jordan Samouh (jordansamouh) - - Baptiste Lafontaine (magnetik) - - Jakub Kucharovic (jkucharovic) - - Edi ModriΔ‡ (emodric) + - Koen Reiniers (koenre) + - Nathan Dench (ndenc2) + - Gijs van Lammeren + - Matthew Grasmick + - David Badura (davidbadura) - Uwe JΓ€ger (uwej711) - Eugene Leonovich (rybakit) - - Filippo Tessarotto + - Damien Alexandre (damienalexandre) - Joseph Rouff (rouffj) - FΓ©lix Labrecque (woodspire) - GordonsLondon - - Jan Sorgalla (jsor) + - Roman Anasal + - Piotr Kugla (piku235) + - Quynh Xuan Nguyen (seriquynh) - Ray + - Philipp Cordes (corphi) + - Andrii Dembitskyi - Chekote - - Antoine Makdessi (amakdessi) + - bhavin (bhavin4u) + - Pavel Popov (metaer) - Thomas Adam - - Jhonny Lidfors (jhonne) - - Diego AgullΓ³ (aeoris) + - R. Achmad Dadang Nur Hidayanto (dadangnh) + - Stefan Kruppa + - Petr Duda (petrduda) + - Marcos Rezende (rezende79) - jdhoek - - David PrΓ©vot + - Ivan Kurnosov + - Dieter - Bob den Otter (bopp) + - Johan Vlaar (johjohan) - Thomas Schulz (king2500) - - Frank de Jonge (frenkynet) - - Nikita Konstantinov - - Wodor Wodorski - - Thomas Lallement (raziel057) - - mcfedr (mcfedr) - - Colin O'Dell (colinodell) + - Benjamin Morel + - Bernd Stellwag + - Frank de Jonge + - Chris Tanaskoski + - julien57 + - LoΓ―c FrΓ©mont (loic425) + - Ippei Sumida (ippey_s) + - Ben Ramsey (ramsey) + - Matthieu Auger (matthieuauger) + - KΓ©vin THERAGE (kevin_therage) + - Josip Kruslin (jkruslin) - Giorgio Premi - renanbr + - SΓ©bastien Lavoie (lavoiesl) - Alex Rock (pierstoval) - - Ben Davies (bendavies) + - Wodor Wodorski - Beau Simensen (simensen) - - Michael Hirschler (mvhirsch) - Robert Kiss (kepten) - - Zan Baldwin (zanderbaldwin) - - Roumen Damianoff (roumen) + - Zan Baldwin (zanbaldwin) + - Antonio J. GarcΓ­a Lagar (ajgarlag) + - Alexandre Quercia (alquerci) + - Marcos SΓ‘nchez + - JΓ©rΓ΄me Tanghe (deuchnord) - Kim HemsΓΈ Rasmussen (kimhemsoe) + - Maximilian Reichel (phramz) + - Dane Powell + - jaugustin + - Dmytro Borysovskyi (dmytr0) + - Mathias STRASSER (roukmoute) - Pascal Luna (skalpa) - Wouter Van Hecke - - JΓ©rΓ΄me Parmentier (lctrs) - Peter Kruithof (pkruithof) - Michael Holm (hollo) - - Mathieu Lechat - - Marc Weistroff (futurecat) + - Giso Stallenberg (gisostallenberg) + - Blanchon Vincent (blanchonvincent) + - William Arslett (warslett) + - JΓ©rΓ©my REYNAUD (babeuloula) - Christian Schmidt - - Patrick Landolt (scube) - - MatTheCat - - Chad Sikorra (chadsikorra) + - Gonzalo Vilaseca (gonzalovilaseca) + - Vadim Borodavko (javer) + - Haralan Dobrev (hkdobrev) + - Soufian EZ ZANTAR (soezz) + - Jan van Thoor (janvt) + - Martin Kirilov (wucdbm) + - Axel Guckelsberger (guite) - Chris Smith (cs278) - Florian Klein (docteurklein) - - Stadly + - Bilge + - CΔƒtΔƒlin Dan (dancatalin) + - Rhodri Pugh (rodnaph) - Manuel Kiessling (manuelkiessling) + - Patrick Reimers (preimers) + - Anatoly Pashin (b1rdex) + - Pol Dellaiera (drupol) - Atsuhiro KUBO (iteman) - - Quynh Xuan Nguyen (xuanquynh) - rudy onfroy (ronfroy) - Serkan Yildiz (srknyldz) + - Jeroen Thora (bolle) - Andrew Moore (finewolf) - Bertrand Zuchuat (garfield-fr) - - Sullivan SENECHAL (soullivaneuh) + - Marc Morera (mmoreram) - Gabor Toth (tgabi333) - realmfoo - Thomas Tourlourat (armetiz) - Andrey Esaulov (andremaha) - GrΓ©goire Passault (gregwar) - Jerzy Zawadzki (jzawadzki) - - Wouter J - Ismael Ambrosi (iambrosi) + - Yannick Ihmels (ihmels) + - Saif Eddin G - Emmanuel BORGES (eborges78) - - FranΓ§ois Pluchino (francoispluchino) + - siganushka (siganushka) - Aurelijus ValeiΕ‘a (aurelijus) + - Evert Harmeling (evertharmeling) - Jan Decavele (jandc) - Gustavo Piltcher + - Shakhobiddin + - Grenier KΓ©vin (mcsky_biig) - Stepan Tanasiychuk (stfalcon) - Tiago Ribeiro (fixe) - - Hidde Boomsma (hboomsma) - - John Bafford (jbafford) - Raul Fraile (raulfraile) - Adrian Rudnik (kreischweide) + - Pavel Batanov (scaytrase) - Francesc RosΓ s (frosas) - - Romain Pierre (romain-pierre) - - Julien Galenski (ruian) - Bongiraud Dominique - janschoenherr + - Marko Kaznovac (kaznovac) - Emanuele Gaspari (inmarelibero) - Dariusz RumiΕ„ski - - Berny Cantos (xphere81) - - Thierry Thuon (lepiaf) - - Ricard Clau (ricardclau) - - dFayet - - Mark Challoner (markchalloner) - - Gennady Telegin (gtelegin) + - Terje BrΓ₯ten + - Gennadi Janzen + - James Hemery + - Egor Taranov + - Philippe Segatori + - Adrian Nguyen (vuphuong87) + - benjaminmal + - Thierry T (lepiaf) + - Lorenz Schori + - Andrey Sevastianov + - Oleksandr Barabolia (oleksandrbarabolia) + - Khoo Yong Jun + - Christin Gruber (christingruber) + - Jeremy Livingston (jeremylivingston) + - Julien Turby + - scyzoryck + - Greg Anderson + - Tri Pham (phamuyentri) + - marie + - Erkhembayar Gantulga (erheme318) + - Fractal Zombie + - Gunnstein Lye (glye) + - Thomas Talbot (ioni) + - NoΓ©mi SalaΓΌn (noemi-salaun) + - Michel Hunziker + - Krystian Marcisz (simivar) + - Matthias Krauser (mkrauser) - Erin Millard - - Artur Melo (restless) + - Lorenzo Millucci (lmillucci) + - JΓ©rΓ΄me Tamarelle (jtamarelle-prismamedia) + - Emil Masiakowski + - Alexandre Parent + - DT Inier (gam6itko) - Matthew Lewinski (lewinski) - Magnus Nordlander (magnusnordlander) + - Ricard Clau (ricardclau) + - Dmitrii Tarasov (dtarasov) + - Philipp Kolesnikov + - Maxim Dovydenok (shiftby) + - Carlos Pereira De Amorim (epitre) + - Rodrigo Aguilera + - Roumen Damianoff + - Vladimir Varlamov (iamvar) - Thomas Royer (cydonia7) + - Gildas QuΓ©mΓ©ner (gquemener) - Nicolas LEFEVRE (nicoweb) - - alquerci + - Asmir Mustafic (goetas) + - Martins Sipenko + - Guilherme Augusto Henschel + - Mardari Dorel (dorumd) + - Pierrick VIGNAND (pierrick) - Mateusz Sip (mateusz_sip) + - Andy Palmer (andyexeter) + - Marko H. Tamminen (gzumba) - Francesco Levorato - - Dmitrii Poddubnyi (karser) + - DerManoMann + - David Molineus + - Desjardins JΓ©rΓ΄me (jewome62) - Vitaliy Zakharov (zakharovvi) - Tobias SjΓΆsten (tobiassjosten) - Gyula Sallai (salla) + - Stefan Gehrig (sgehrig) + - Benjamin Cremer (bcremer) + - rtek - Inal DJAFAR (inalgnu) - Christian GΓ€rtner (dagardner) + - Artem Stepin (astepin) + - Adrien Jourdier (eclairia) + - Ivan Grigoriev (greedyivan) - Tomasz Kowalczyk (thunderer) + - Erik Saunier (snickers) + - Kevin SCHNEKENBURGER + - Fabien Salles (blacked) - Artur Eshenbrener - - Andreas Braun - - Arjen van der Meijden - - Damien Alexandre (damienalexandre) + - Ahmed Ashraf (ahmedash95) + - Gert Wijnalda (cinamo) + - Luca Saba (lucasaba) - Thomas Perez (scullwm) + - Thomas P + - Kristijan KanalaΕ‘ (kristijan_kanalas_infostud) - Felix Labrecque + - mondrake (mondrake) - Yaroslav Kiliba + - β€œFilip + - Simon Watiau (simonwatiau) + - Ruben Jacobs (rubenj) + - Arkadius Stefanski (arkadius) + - JΓ©rΓ©my M (th3mouk) - Terje BrΓ₯ten + - Pierre Rineau + - Renan GonΓ§alves (renan_saddam) + - Raulnet + - Tomasz Kusy + - Jakub Kucharovic (jkucharovic) + - Kristen Gilden + - Oleksiy (alexndlm) - Robbert Klarenbeek (robbertkl) - Eric Masoero (eric-masoero) - - JhonnyL - - David Badura (davidbadura) + - Michael Lutz + - Michel Roca (mroca) + - Reedy - hossein zolfi (ocean) - ClΓ©ment Gautier (clementgautier) - - Sanpi + - Jelle Raaijmakers (gmta) + - Roberto Nygaard + - Joshua Nye + - Dalibor KarloviΔ‡ + - Randy Geraads + - Sanpi (sanpi) + - James Gilliland (neclimdul) - Eduardo Gulias (egulias) + - Andreas Leathley (iquito) + - Nathanael Noblet (gnat) - giulio de donato (liuggio) + - Mohamed Gamal + - Eric COURTIAL + - Xesxen - ShinDarth + - Arun Philip - StΓ©phane PY (steph_py) - Philipp KrΓ€utli (pkraeutli) + - Carl Casbolt (carlcasbolt) + - battye + - BrokenSourceCode - Grzegorz (Greg) Zdanowski (kiler129) - - Iker Ibarguren (ikerib) - - Kirill chEbba Chebunin (chebba) + - Kirill chEbba Chebunin + - kylekatarnls (kylekatarnls) + - Steve Grunwell + - + - Alex (aik099) - Greg Thornton (xdissent) - - Martin Hujer (martinhujer) - - Philipp Cordes + - BENOIT POLASZEK (bpolaszek) + - Shaharia Azam + - Gerben Oolbekkink + - Alexandre Parent + - Thibault Richard (t-richard) + - Oleksii Zhurbytskyi + - Guillaume Verstraete + - vladimir.panivko + - Jason Tan (jt2k) - Costin Bereveanu (schniper) - - LoΓ―c Chardonnet (gnusat) + - kick-the-bucket - Marek Kalnik (marekkalnik) + - Jeremiasz Major - Vyacheslav Salakhutdinov (megazoll) + - Trevor North + - Maksym Slesarenko (maksym_slesarenko) - Hassan Amouhzi + - Antonin CLAUZIER (0x346e3730) + - Andrei C. (moldman) - Tamas Szijarto + - stlrnz + - Adrien Wilmet (adrienfr) + - Alex Bacart + - hugovms - Michele Locati - Pavel Volokitin (pvolok) - - Smaine Milianni (ismail1432) + - DemigodCode - Arthur de Moulins (4rthem) - Matthias Althaus (althaus) - - Nicolas Dewez (nicolas_dewez) + - Maximilian BΓΆsing + - Leevi Graham (leevigraham) - Endre Fejes + - Carlos Buenosvinos (carlosbuenosvinos) + - Jake (jakesoft) - Tobias Naumann (tna) + - Greg ORIOL + - Bahman Mehrdad (bahman) - Daniel Beyer + - Manuel Alejandro Paz Cetina + - Youssef Benhssaien (moghreb) + - Mario Ramundo (rammar) + - Ivan - Shein Alexey - - Romain Gautier (mykiwi) + - Nico Haase + - Jacek JΔ™drzejewski (jacek.jedrzejewski) + - Stefan Kruppa + - Shahriar56 + - Dhananjay Goratela + - Kien Nguyen - Joe Lencioni + - arai + - Mouad ZIANI (mouadziani) - Daniel Tschinder + - Diego AgullΓ³ (aeoris) + - Tomasz Ignatiuk + - Joachim LΓΈvgaard (loevgaard) - vladimir.reznichenko - - Ruud Kamphuis (ruudk) - Kai - Lee Rowlands - - Krzysztof Piasecki (krzysztek) - - Maximilian Reichel (phramz) - - Loick Piera (pyrech) + - Alain Hippolyte (aloneh) - Karoly Negyesi (chx) - - Ivan Kurnosov - Xavier HAUSHERR + - Loïc Beurlet + - Ana Raro + - Ana Raro + - Tom Klingenberg + - Florian Wolfsjaeger (flowolf) + - Ivan Sarastov (isarastov) + - Jordi Sala Morales (jsala) - Albert Jessurum (ajessu) + - Samuele Lilli (doncallisto) + - Peter Bowyer (pbowyer) + - Romain Pierre - Laszlo Korte - - Miha Vrhovnik + - Gabrielle Langer - Alessandro Desantis - hubert lecorche (hlecorche) + - bogdan + - mmokhi + - Daniel Tiringer + - Andrew Codispoti + - Lctrs + - Joppe De Cuyper (joppedc) - Marc Morales ValldepΓ©rez (kuert) - - Jean-Baptiste GOMOND (mjbgo) - - Vadim Kharitonov (virtuozzz) + - Vadim Kharitonov (vadim) - Oscar Cubo Medina (ocubom) - Karel Souffriau - Christophe L. (christophelau) + - DaniΓ«l Brekelmans (dbrekelmans) + - Simon Heimberg (simon_heimberg) + - Morten Wulff (wulff) + - Sander Toonen (xatoo) - Anthon Pang (robocoder) - - Michael KΓ€fer (michael_kaefer) - - SΓ©bastien Santoro (dereckson) + - Julien Galenski (ruian) + - Rimas Kudelis + - Ben Scott (bpscott) + - Andrii Dembitskyi + - a.dmitryuk + - Pavol Tuka + - Paulo Ribeiro (paulo) + - Marc Laporte + - MichaΕ‚ JusiΔ™ga + - Dmitriy Derepko + - Sebastian Paczkowski (sebpacz) + - Dragos Protung (dragosprotung) + - Thiago Cordeiro (thiagocordeiro) + - Julien Maulny - Brian King - - Michel Salib (michelsalib) - - geoffrey + - Paul Oms - Steffen Roßkamp - Alexandru Furculita (afurculita) - - Valentin Jonovs (valentins-jonovs) - - Laurent VOULLEMIER (lvo) + - Michel Salib (michelsalib) + - Valentin Jonovs + - geoffrey + - Bastien DURAND (deamon) + - Benoit Galati (benoitgalati) + - Jon Gotlin (jongotlin) - Jeanmonod David (jeanmonod) - - Christopher Davis (chrisguitarguy) + - Daniel GonzΓ‘lez (daniel.gonzalez) + - Renan (renanbr) - Webnet team (webnet) + - Tobias BΓΆnner + - Berny Cantos (xphere81) + - MΓ‘tyΓ‘s Somfai (smatyas) - Jan Schumann + - Matheo Daninos (mathdns) - Niklas Fiekas + - Mark Challoner (markchalloner) - Markus Bachmann (baachi) + - Philippe SEGATORI (tigitz) + - Roger Guasch (rogerguasch) + - Luis TacΓ³n (lutacon) + - Alex Hofbauer (alexhofbauer) + - Andrii Popov (andrii-popov) - lancergr - - Mihai Stancu - Ivan Nikolaev (destillat) - - Olivier Dolbeau (odolbeau) - - Jan Rosier (rosier) - - Alessandro Lai (jean85) + - Xavier Leune (xleune) + - Ben Roberts (benr77) + - Joost van Driel (j92) + - ampaze - Arturs Vonda - - Josip Kruslin - - Asmir Mustafic (goetas) + - Xavier Briand (xavierbriand) + - Daniel Badura - vagrant - - Aurimas Niekis (gcds) - - EdgarPE - - Florian Pfitzer (marmelatze) - Asier Illarramendi (doup) + - AKeeman (akeeman) - Martijn Cuppens + - Restless-ET - Vlad Gregurco (vgregurco) - - Maciej Malarz (malarzm) - Boris Vujicic (boris.vujicic) - Chris Sedlmayr (catchamonkey) - - Dmytro Borysovskyi (dmytr0) - Kamil Kokot (pamil) - Seb Koelen + - FORT Pierre-Louis (plfort) - Christoph Mewes (xrstf) - Vitaliy Tverdokhlib (vitaliytv) - Ariel Ferrandini (aferrandini) + - Niklas Keller - Dirk Pahl (dirkaholic) - - cedric lombardot (cedriclombardot) - - Tim Goudriaan (codedmonkey) + - CΓ©dric Lombardot (cedriclombardot) - Jonas FlodΓ©n (flojon) - - Gonzalo Vilaseca (gonzalovilaseca) - - Marcin SikoΕ„ (marphi) - - Denis Brumann (dbrumann) - - Dominik Zogg (dominik.zogg) - - Marek Pietrzak + - Adrien Lucas (adrienlucas) + - Dominik Zogg + - Kai Dederichs - Luc Vieillescazes (iamluc) - - franek (franek) - - Christian Wahler - - Gintautas Miselis + - Thomas Nunninger + - FranΓ§ois Dume (franek) - Rob Bast - Roberto Espinoza (respinoza) - - Zander Baldwin - Adam Harvey + - ilyes kooli (skafandri) - Anton Bakai - - Rhodri Pugh (rodnaph) - Sam Fleming (sam_fleming) - Alex Bakhturin - - Pol Dellaiera (drupol) + - Brayden Williams (redstar504) - insekticid - - Alexander Obuhovich (aik099) + - Trent Steel (trsteel88) + - Vitaliy Ryaboy (vitaliy) - boombatower - - Fabrice Bernhard (fabriceb) + - Douglas Hammond (wizhippo) - JΓ©rΓ΄me Macias (jeromemacias) - Andrey Astakhov (aast) - ReenExe - Fabian Lange (codingfabian) - - Frank Neff (fneff) - - Roman Lapin (memphys) - Yoshio HANAWA - - Gladhon - - Haralan Dobrev (hkdobrev) + - Toon Verwerft (veewee) + - Gert de Pagter - Sebastian Bergmann - - Miroslav Sustek + - Miroslav Ε ustek (sustmi) - Pablo DΓ­ez (pablodip) + - Damien Fa - Kevin McBride - Sergio Santoro - - Robin van der Vleuten (robinvdvleuten) + - AndrolGenhald - Philipp Rieber (bicpi) - - Tomas NorkΕ«nas (norkunas) + - Dennis VΓ¦versted (srnzitcom) - Manuel de Ruiter (manuel) + - nikos.sotiropoulos - Eduardo Oliveira (entering) - - Ilya Antipenko (aivus) + - Jonathan Johnson (jrjohnson) + - Eugene Wissner - Ricardo Oliveira (ricardolotr) - Roy Van Ginneken (rvanginneken) - ondrowan - Barry vd. Heuvel (barryvdh) - - Craig Duncan (duncan3dc) - - SΓ©bastien Alfaiate (seb33300) + - Jon Dufresne + - Chad Sikorra (chadsikorra) + - Mathias Brodala (mbrodala) + - naitsirch (naitsirch) - Evan S Kaufman (evanskaufman) + - Jonathan Sui Lioung Lee Slew (jlslew) - mcben - JΓ©rΓ΄me Vieilledent (lolautruche) - - Maks Slesarenko - Filip ProchΓ‘zka (fprochazka) - - mmoreram + - stoccc - Markus Lanthaler (lanthaler) + - Gigino Chianese (sajito) + - Xav` (xavismeh) - Remi Collet + - Mathieu Rochette (mathroc) - Vicent Soria DurΓ‘ (vicentgodella) - Michael Moravec - Anthony Ferrara + - Glodzienski + - Christian Gripp (core23) + - Marcel Hernandez - Ioan Negulescu - Jakub Ε kvΓ‘ra (jskvara) - Andrew Udvare (audvare) + - Volodymyr Panivko - alexpods - - Saif Eddin G - - Adam Szaraniec (mimol) + - Dennis Langen (nijusan) + - Adam Szaraniec - Dariusz Ruminski - - Erik Trapman (eriktrapman) - - Rokas MikalkΔ—nas (rokasm) + - Romain Gautier (mykiwi) + - Cyril Pascal (paxal) + - Matthieu Bontemps + - Erik Trapman - De Cock Xavier (xdecock) - - Almog Baku (almogbaku) + - Nicolas Dewez (nicolas_dewez) + - Quentin Dreyer - Scott Arciszewski - Xavier HAUSHERR - - Christopher Hertel (chertel) + - Achilles Kaloeridis (achilles) - Norbert Orzechowicz (norzechowicz) - - Denis Charrier (brucewouaigne) + - Robert-Jan de Dreu + - Fabrice Bernhard (fabriceb) - Matthijs van den Bos (matthijs) - Jaik Dean (jaikdean) + - Krzysztof Piasecki (krzysztek) - Lenard Palko - Nils Adermann (naderman) - GΓ‘bor FΓ‘si - - DUPUCH (bdupuch) - - Benjamin Leveque (benji07) - Nate (frickenate) - - TimothΓ©e Barray (tyx) - - jhonnyL - - Grenier KΓ©vin (mcsky_biig) - sasezaki + - Kristof Van Cauwenbergh (kristofvc) - Dawid PakuΕ‚a (zulusx) + - Marco Lipparini (liarco) - Florian Rey (nervo) - Rodrigo Borrego BernabΓ© (rodrigobb) + - John Bafford (jbafford) - Emanuele Iannone - - JΓΆrn Lang (j.lang) + - Gasan Guseynov (gassan) + - Ondrej Machulda (ondram) - Denis Gorbachev (starfall) - - Peter van Dommelen - - Tim van Densen - Martin MorΓ‘vek (keeo) - - Steven Surowiec - Kevin Saliou (kbsali) + - Matthieu Mota (matthieumota) + - Steven Surowiec (steves) - Shawn Iwinski - Gawain Lynch (gawain) - - NothingWeAre - Ryan - Alexander Deruwe (aderuwe) - - Alain Hippolyte (aloneh) - Dave Hulbert (dave1010) + - Konstantin Grachev (grachevko) - Ivan Rey (ivanrey) + - M. (mbontemps) - Marcin ChyΕ‚ek (songoq) - - Ben Scott - Ned Schwartz + - Anderson MΓΌller - Ziumin - - Jeremy Benoist - - fritzmg + - Matthias Schmidt - Lenar LΓ΅hmus - - Sander Toonen (xatoo) - - Benjamin Laugueux (yzalis) + - Ilija Tovilo (ilijatovilo) + - SamaΓ«l Villette (samadu61) - Zach Badgett (zachbadgett) + - LoΓ―c Faugeron - AurΓ©lien Fredouelle - Pavel Campr (pcampr) + - Forfarle (forfarle) - Johnny Robeson (johnny) - - Marko Kaznovac (kaznovac) + - Kai Eichinger (kai_eichinger) + - Kuba WerΕ‚os (kuba) + - Philipp Keck - Disquedur - - Michiel Boeckaert (milio) + - Markus S. (staabm) + - Guilherme Ferreira - Geoffrey Tran (geoff) - - Jan Behrens + - Elan RuusamΓ€e (glen) + - Brad Jones + - Nicolas de MarquΓ© (nicola) + - Jannik Zschiesche + - Jan Ole Behrens (deegital) - Mantas Var (mvar) + - Yann LUCAS (drixs6o9) - Sebastian Krebs + - Htun Htun Htet (ryanhhh91) + - Sorin Pop (sorinpop) - Piotr Stankowski - - Baptiste Leduc (bleduc) - - Jean-Christophe Cuvelier [Artack] - - Simon DELICATA + - Stewart Malik + - Stefan Graupner (efrane) + - Gemorroj (gemorroj) + - Adrien Chinour + - Mihail Krasilnikov (krasilnikovm) + - iamvar + - Pierre Tondereau + - Joel Lusavuvu (enigma97) + - Alex Vo (votanlean) + - AndrΓ© Matthies + - Piergiuseppe Longo + - Kevin Auivinet + - Valentin Nazarov + - AurΓ©lien MARTIN + - Malte Schlüter + - Jules Matsounga (hyoa) + - Quentin Dequippe (qdequippe) + - Yewhen Khoptynskyi (khoptynskyi) + - JΓ©rΓ΄me Nadaud (jnadaud) + - wuchen90 + - Alexandre Tranchant (alexandre_t) + - Anthony Moutte + - shreyadenny + - Daniel Iwaniec + - Thomas Ferney (thomasf) + - Hallison Boaventura (hallisonboaventura) + - Mas Iting + - Albion Bame (abame) + - Ivan Nemets - Dmitry Simushev + - GrΓ©goire HΓ©bert (gregoirehebert) - alcaeus - Fred Cox + - Iliya Miroslavov Iliev (i.miroslavov) + - Safonov Nikita (ns3777k) + - Simon DELICATA + - Thibault Buathier (gwemox) + - Julien Boudry - vitaliytv - - Dalibor KarloviΔ‡ (dkarlovi) + - Andreas Hennings + - Arnaud FrΓ©zet + - Nicolas Martin (cocorambo) + - luffy1727 + - LHommet Nicolas (nicolaslh) - Sebastian Blum - - aubx - - Marvin Butkereit - - Renan + - Amirreza Shafaat (amirrezashafaat) + - Laurent Clouet + - Adoni Pavlakis (adoni) + - Maarten Nusteling (nusje2000) + - Ahmed EBEN HASSINE (famas23) + - Eduard Bulava (nonanerz) - Ricky Su (ricky) - - Gildas QuΓ©mΓ©ner (gquemener) + - Igor Timoshenko (igor.timoshenko) + - β€œteerasak” - Kyle Evans (kevans91) - - Charles-Henri Bruyand + - Benoit Mallo - Max Rath (drak3) + - Giuseppe Campanelli + - Valentin + - pizzaminded + - Matthieu Calie (matth--) - StΓ©phane Escandell (sescandell) - - Konstantin S. M. MΓΆllers (ksmmoellers) + - ivan + - linh + - Oleg Krasavin (okwinza) + - Mario BlaΕΎek (marioblazek) + - Jure (zamzung) - James Johnston + - Michael Nelson + - Eric Krona - Sinan Eldem + - Gennady Telegin + - Kajetan KoΕ‚tuniak (kajtii) + - Sander Goossens (sandergo90) + - Damien Fayet (rainst0rm) - Alexandre Dupuy (satchette) + - MatTheCat - Malte BlΓ€ttermann - - Desjardins JΓ©rΓ΄me (jewome62) - - KΓ©vin THERAGE (kevin_therage) + - Erfan Bahramali - Simeon Kolev (simeon_kolev9) + - Abdiel Carrazana (abdielcs) + - Arman + - Gabi Udrescu + - Adamo Crespi (aerendir) - Jonas Elfering + - Luis Pabon (luispabon) + - boulei_n + - Anna Filina (afilina) + - Mihai Stancu - Nahuel Cuesta (ncuesta) + - Patrick Luca Fazzi (ap3ir0n) - Chris Boden (cboden) + - EStyles (insidestyles) - Christophe Villeger (seragan) + - Bruno Rodrigues de Araujo (brunosinister) - Julien Fredon - - Bob van de Vijver (bobvandevijver) - - Stefan Gehrig (sgehrig) + - Jacek WilczyΕ„ski (jacekwilczynski) - Hany el-Kerdany - Wang Jingyu + - Benjamin Georgeault (wedgesama) - Γ…smund Garfors - - Gunnstein Lye (glye) - Maxime Douailin - - Jean Pasdeloup (pasdeloup) - - Sylvain Fabre (sylfabre) - - Benjamin Cremer (bcremer) + - Jean Pasdeloup + - Laurent Moreau + - Michael Hirschler (mvhirsch) - Javier LΓ³pez (loalf) + - tamar peled - Reinier Kip - Geoffrey Brier (geoffrey-brier) + - Sofien Naas + - Christophe Meneses (c77men) - Vladimir Tsykun + - Andrei O - Dustin Dobervich (dustin10) + - Alejandro Diaz Torres + - Karl Shea - dantleech - - Anne-Sophie Bachelard (annesophie) + - Valentin - Sebastian Marek (proofek) - - Guilhem N (guilhemn) - - Erkhembayar Gantulga (erheme318) + - Łukasz ChruΕ›ciel (lchrusciel) + - Jan Vernieuwe (vernija) - zenmate - - Michal Trojanowski + - j.schmitt + - Georgi Georgiev - David Fuhr - - Max Grigorian (maxakawizard) - - DerManoMann + - Evgeny Anisiforov + - TristanPouliquen + - Gwendolen Lynch + - mwos + - Aurimas Niekis (gcds) + - Volker Killesreiter (ol0lll) + - Vedran Mihočinec (v-m-i) + - creiner + - RevZer0 (rav) + - remieuronews + - Marek Binkowski - Rostyslav Kinash + - Andrey Lebedev (alebedev) + - Cristoforo Cervino (cristoforocervino) - Dennis Fridrich (dfridrich) + - Yoann MOROCUTTI - Daisuke Ohata - Vincent Simonin + - Alexander Onatskiy + - Philipp Fritsche + - tarlepp - Alex Bogomazov (alebo) - - maxime.steinhausser - - adev - - Stefan Warman - - Arkadius Stefanski (arkadius) + - Claus Due (namelesscoder) + - aaa2000 (aaa2000) + - Guillaume Aveline + - Alexandru Patranescu + - Arkadiusz Rzadkowolski (flies) + - Oksana Kozlova (oksanakozlova) + - Quentin Moreau (sheitak) + - Stefan Warman (warmans) + - Bert Ramakers + - Angelov Dejan (angelov) - Tristan Maindron (tmaindron) - Behnoush Norouzali (behnoush) + - Marc Duboc (icemad) - Wesley Lancel - Ke WANG (yktd26) + - TimothΓ©e BARRAY + - Nilmar Sanchez Muguercia - Ivo Bathke (ivoba) + - Lukas Mencl - Strate - Anton A. Sumin + - Atthaphon Urairat + - Jon Green (jontjs) + - MickaΓ«l Isaert (misaert) - Israel J. Carberry + - Julius Kiekbusch - Miquel RodrΓ­guez Telep (mrtorrent) + - TamΓ‘s Nagy (t-bond) - Sergey Kolodyazhnyy (skolodyazhnyy) - umpirski + - Benjamin - Quentin de Longraye (quentinus95) - Chris Heng (gigablah) - - Shaun Simmons (simshaun) + - Oleksii Svitiashchuk + - Tristan Bessoussa (sf_tristanb) - Richard Bradley - - Ulumuddin Yunus (joenoez) + - NathanaΓ«l Martel (nathanaelmartel) + - Nicolas Jourdan (nicolasjc) + - Ulumuddin Cahyadi Yunus (joenoez) + - Benjamin Dos Santos + - GagnarTest (gagnartest) + - Tomas Javaisis + - Florian Pfitzer (marmelatze) - Johann Saunier (prophet777) - - Sergey (upyx) - - Andreas Erhard + - Lucas Bäuerle + - Dario Savella + - Jack Thomas + - Andreas Erhard (andaris) + - Evgeny Efimov (edefimov) + - John VanDeWeghe + - Oleg Mifle + - gnito-org - Michael Devery (mickadoo) + - LoΓ―c Ovigne (oviglo) - Antoine Corcy - - Sascha Grossenbacher - - Emanuele Panzeri (thepanz) + - Markkus Millend + - ClΓ©ment + - Jorrit Schippers (jorrit) + - Aurimas Niekis (aurimasniekis) + - maxime.perrimond + - rvoisin + - cthulhu + - Sascha Grossenbacher (berdir) + - Dmitry Derepko + - RΓ©mi Leclerc + - Jan Vernarsky + - Jonas HΓΌnig + - Amine Yakoubi + - Robin Lehrmann - Szijarto Tamas - - Gocha Ossinkine (ossinkine) - - Robin Lehrmann (robinlehrmann) - - Catalin Dan + - Arend Hummeling + - Makdessi Alex + - Juan Miguel Besada Vidal (soutlink) + - dlorek + - Stuart Fyfe - Jaroslav Kuba - - Stephan Vock - Benjamin Zikarsky (bzikarsky) - - battye + - Jason Schilling (chapterjason) + - Nathan PAGE (nathix) + - sl_toto (sl_toto) + - Marek Pietrzak (mheki) + - Dmitrii Lozhkin + - Marion Hurteau (marionleherisson) + - MickaΓ«l Andrieu (mickaelandrieu) + - Oscar Esteve (oesteve) + - Sobhan Sharifi (50bhan) + - Peter Potrowl + - Stephen + - Tomasz (timitao) + - Nguyen Tuan Minh (tuanminhgp) + - dbrekelmans + - Piet Steinhart + - mousezheng + - RΓ©my LESCALLIER - Simon Schick (simonsimcity) - - redstar504 + - Victor Macko (victor_m) - Tristan Roussel + - Quentin Devos + - Jorge Vahldick (jvahldick) + - Vladimir Mantulo (mantulo) + - aim8604 + - Aleksandr Dankovtsev + - Maciej Zgadzaj + - David Legatt (dlegatt) + - Maarten de Boer (mdeboer) - Cameron Porter - Hossein Bukhamsin - Oliver Hoff - Christian Sciberras (uuf6429) - - Martin Auswöger - - Disparity + - Arthur WoimbΓ©e + - ThΓ©o DELCEY + - Andrii Serdiuk (andreyserdjuk) + - dangkhoagms (dangkhoagms) + - Floran Brutel (notFloran) (floran) + - Vlad Gapanovich (gapik) - origaminal - Matteo Beccati (matteobeccati) + - Konstantin Bogomolov + - Mark Spink + - Cesar Scur (c 8000 esarscur) - Kevin (oxfouzer) - PaweΕ‚ WacΕ‚awczyk (pwc) + - Sagrario Meneses - Oleg Zinchenko (cystbear) - Baptiste Meyer (meyerbaptiste) + - Stefano A. (stefano93) - Tales Santos (tsantos84) - Johannes Klauss (cloppy) + - PierreRebeilleau - Evan Villemez + - Florian Hermann (fhermann) - fzerorubigd - Thomas Ploch - Benjamin Grandfond (benjamin) - Tiago Brito (blackmx) - - + - Gintautas Miselis (naktibalda) + - Christian RishΓΈj + - Roromix + - Patrick Berenschot + - SuRiKmAn + - rtek + - Maxime AILLOUD (mailloud) - Richard van den Brand (ricbra) - - Thomas Bisignani (toma) + - Sergey Melesh (sergex) + - mohammadreza honarkhah - develop - flip111 - - Greg Anderson + - Artem Oliinyk (artemoliynyk) + - Marvin Feldmann (breyndotechse) + - fruty - VJ - RJ Garcia + - Adam WΓ³js (awojs) + - Justin Reherman (jreherman) - Delf Tonder (leberknecht) - - Raulnet + - PaweΕ‚ Niedzielski (steveb) + - Peter Jaap Blaakmeer + - Agustin Gomes - Ondrej Exner - Mark Sonnabaum + - Adiel Cristo (arcristo) + - Fabian Kropfhamer (fabiank) + - Junaid Farooq (junaidfarooq) + - Chris Jones (magikid) - Massimiliano Braglia (massimilianobraglia) + - Swen van Zanten (swenvanzanten) + - Frankie Wittevrongel - Richard Quadling + - James Hudson (mrthehud) + - Adam Prickett - RaphaΓ«ll Roussel + - Luke Towers + - Anton Kroshilin + - Norman Soetbeer + - William Thomson (gauss) + - Javier Espinosa (javespi) - jochenvdv + - FrantiΕ‘ek MaΕ‘a - Arturas Smorgun (asarturas) - - Alexander Volochnev (exelenz) - - Michael Piecko + - Andrea Sprega (asprega) + - Aleksandr Volochnev (exelenz) + - Viktor Bajraktar (njutn95) + - Robin van der Vleuten (robinvdvleuten) + - Grinbergs Reinis (shima5) + - Ruud Arentsen + - Harald Tollefsen + - Arend-Jan Tetteroo + - Mbechezi Nawo + - Andre Eckardt (korve) + - Michael Piecko (michael.piecko) + - Osayawe Ogbemudia Terry (terdia) - Toni Peric (tperic) - yclian - - Alan Poulain - Aleksey Prilipko - Andrew Berry - - twifty - - Indra Gunawan (guind) - - Peter Ward + - Wybren Koelmans (wybren_koelmans) + - Dmytro Dzubenko + - Benjamin RICHARD + - pdommelen + - Cedrick Oka - Davide Borsatto (davide.borsatto) + - Guillaume Sainthillier (guillaume-sainthillier) + - Jens Hatlak + - Tayfun Aydin + - Arne Groskurth + - Ilya Chekalsky + - zenas1210 + - Ostrzyciel - Julien DIDIER (juliendidier) - - Dominik Ritter (dritter) + - Ilia Sergunin (maranqz) + - Johan de Ruijter + - marbul + - Filippos Karailanidis + - David Brooks + - Volodymyr Kupriienko (greeflas) - Sebastian Grodzicki (sgrodzicki) - - Jeroen van den Enden (stoefke) - - nikos.sotiropoulos + - Florian Caron (shalalalala) + - Serhiy Lunak (slunak) + - Wojciech BΕ‚oszyk (wbloszyk) + - Jeroen van den Enden (endroid) + - Jiri Barous + - abunch + - tamcy + - Mikko Pesari + - AurΓ©lien Fontaine - Pascal Helfenstein - - Anthony GRASSIOT (antograssiot) + - Malcolm Fell (emarref) + - phuc vo (phucwan) - Baldur Rensch (brensch) - - Pierre Rineau + - Bogdan Scordaliu + - Daniel Rotter (danrot) + - Foxprodev + - developer-av - Vladyslav Petrovych + - LoΓ―c Chardonnet + - Hugo Sales + - Dale.Nash - Alex Xandra Albert Sim - - Carson Full - Sergey Yastrebov - - Trent Steel (trsteel88) + - Carson Full (carsonfull) - Yuen-Chi Lian + - Maxim Semkin + - BrokenSourceCode + - Fabian Haase + - Nikita Popov (nikic) + - Robert Fischer (sandoba) - Tarjei Huse (tarjei) - Besnik Br + - Michael Olšavský + - Benny Born + - Emirald Mateli + - Tristan Pouliquen - Jose Gonzalez - - Oleksii Zhurbytskyi - - Dariusz Ruminski - - Joshua Nye - Claudio Zizza + - Ivo Valchev + - Zlatoslav Desyatnikov + - Wickex + - tuqqu + - Neagu Cristian-Doru (cristian-neagu) - Dave Marshall (davedevelopment) - Jakub Kulhan (jakubkulhan) - avorobiev + - Gladhon + - Kai + - BartΕ‚omiej ZajΔ…c + - Maximilian.Beckers - GrΓ©goire Penverne (gpenverne) - Venu - - Lars Vierbergen - Jonatan MΓ€nnchen - Dennis Hotson - Andrew Tchircoff (andrewtch) + - Lars Vierbergen (vierbergenlars) + - Barney Hanlon + - Bart Wach + - Jos Elstgeest + - Kirill Lazarev + - Serhii Smirnov + - Martins Eglitis - michaelwilliams + - Wouter Diesveld + - Romain + - MatΔ›j HumpΓ‘l + - Guillaume Loulier (guikingone) + - Pedro Casado (pdr33n) + - Pierre Grimaud (pgrimaud) + - Alexander Janssen (tnajanssen) - 1emming - - Leevi Graham (leevigraham) - Nykopol (nykopol) - - Tri Pham (phamuyentri) + - Julien BERNARD + - Michael Zangerle - Jordan Deitch + - Raphael Hardt - Casper Valdemar Poulsen + - SnakePin + - Matthew Covey + - Anthony Massard (decap94) + - Chris Maiden (matason) + - Andrea Ruggiero (pupax) - Josiah (josiah) - - Greg ORIOL + - Alexandre Beaujour + - George Yiannoulopoulos - Joschi Kuphal - John Bohn (jbohn) - - Marc Morera (mmoreram) - - Saif Eddin Gmati (azjezz) - - BENOIT POLASZEK (bpolaszek) + - Peter Schultz + - Benhssaein Youssef + - bill moll + - PaoRuby + - Bizley + - Edvin Hultberg + - Dominik Piekarski (dompie) + - Rares Sebastian Moldovan (raresmldvn) + - Felds Liscia (felds) + - dsech + - Gilbertsoft + - tadas + - Bastien Picharles + - mamazu + - Victor Garcia + - Juan Mrad + - Denis Yuzhanin + - knezmilos13 + - alireza + - Marcin Kruk + - Marek VΓ­ger (freezy) - Andrew Hilobok (hilobok) + - Wahyu Kristianto (kristories) - Noah Heck (myesain) + - Stephan Wentz (temp) - Christian Soronellas (theunic) - - Johann Pardanaud - fedor.f - Yosmany Garcia (yosmanyga) - - Wouter de Wild + - Markus Staab + - bahram + - Marie Minasyan (marie.minassyan) - Degory Valentine - izzyp - - Benoit LΓ©vΓͺque (benoit_leveque) - Jeroen Fiege (fieg) - - Krzysiek ŁabuΕ› + - Martin (meckhardt) + - RadosΕ‚aw Kowalewski + - JustDylan23 + - buffcode + - Juraj Surman + - Victor + - Andreas Allacher + - Alexis + - Camille Dejoye (cdejoye) + - Krzysztof ŁabuΕ› (crozin) + - cybernet (cybernet2u) + - Stefan Kleff (stefanxl) + - Thijs-jan Veldhuizen (tjveldhuizen) - Xavier Lacot (xavier) - possum - Denis Zunke (donalberto) - - Ahmadou Waly Ndiaye (waly) - - Ahmed TAILOULOUTE (ahmedtai) - - Jonathan Johnson (jrjohnson) - - Olivier Maisonneuve (olineuve) + - _sir_kane (waly) + - Olivier Maisonneuve + - Bruno BOUTAREL + - John Stevenson + - everyx + - Stanislav Gamayunov (happyproff) + - Alexander McCullagh (mccullagh) + - Paul L McNeely (mcneely) + - Mike Meier (mykon) - Pedro Miguel Maymone de Resende (pedroresende) + - Sergey Fokin (tyraelqp) - Masterklavi - Franco Traversaro (belinde) - Francis Turmel (fturmel) - Nikita Nefedov (nikita2206) + - Bernat Llibre + - Daniel Burger - cgonzalez - Ben + - Joni Halme + - aetxebeste + - roromix + - Vitali Tsyrkin + - Juga Paazmaya + - afaricamp + - riadh26 + - Konstantinos Alexiou + - Dilek Erkut + - WaiSkats + - Morimoto Ryosuke + - Christoph KΓΆnig (chriskoenig) + - Dmytro Pigin (dotty) - Vincent Composieux (eko) + - Jm Aribau (jmaribau) - Jayson Xu (superjavason) - - Hubert Lenoir (hubert_lenoir) - fago - - Harm van Tilborg + - popnikos + - Tito Costa - Jan Prieser - - GDIBass - - Antoine Lamirault - - Adrien Lucas (adrienlucas) + - Thiago Melo + - Giorgio Premi + - Matt Johnson (gdibass) + - Gerhard Seidel (gseidel) - Zhuravlev Alexander (scif) - Stefano Degenkamp (steef) - James Michael DuPont - - Tom Klingenberg + - kor3k kor3k (kor3k) + - Eric Schildkamp + - agaktr + - Vincent CHALAMON + - Mostafa + - kernig + - Gennadi Janzen + - SenTisso + - Joe Springe + - Ivan Kurnosov + - Flinsch + - botbotbot + - Timon van der Vorm + - G.R.Dalenoort + - Vladimir Khramtsov (chrome) + - Denys Voronin (hurricane) + - Jordan de Laune (jdelaune) + - Juan Gonzalez Montes (juanwilde) + - Mathieu Dewet (mdewet) - Christopher Hall (mythmakr) + - none (nelexa) - Patrick Dawkins (pjcdawkins) - Paul Kamer (pkamer) - RafaΕ‚ Wrzeszcz (rafalwrzeszcz) - - Vincent CHALAMON (vincentchalamon) + - RΓ©mi Faivre (rfv) + - Nguyen Xuan Quynh - Reen Lokum - - Andreas MΓΆller (localheinz) - Martin Parsiegla (spea) - - Quentin Schuler + - Bernhard Rusch + - Ruben Jansen + - Marc Biorklund + - shreypuranik + - Thibaut Salanon + - Urban Suppiger + - Denis Charrier (brucewouaigne) + - Marcello MΓΆnkemeyer (marcello-moenkemeyer) + - Sander De la Marche (sanderdlm) + - Philipp Scheit (pscheit) - Pierre Vanliefland (pvanliefland) - Roy Klutman (royklutman) - Sofiane HADDAG (sofhad) + - Quentin Schuler (sukei) + - VojtaB - frost-nzcr4 + - Yuri Karaban + - Johan + - Edwin + - Andriy + - Taylor Otwell + - Sami Mussbach + - qzylalala + - Mikolaj Czajkowski + - Shiro + - Reda DAOUDI + - Jesper Skytte + - Christiaan Wiesenekker - Bozhidar Hristov + - Foxprodev + - Eric Hertwig + - Sergey Panteleev + - Dmitry Hordinky + - Oliver Klee + - Niels Robin-Aubertin + - Mikko Ala-Fossi + - Jan Christoph Beyer + - Daniel Tiringer + - Koray Zorluoglu + - Roy-Orbison + - kshida + - Yasmany Cubela Medina (bitgandtter) + - Aryel Tupinamba (dfkimera) + - Hans HΓΆchtl (hhoechtl) + - Jawira Portugal (jawira) - Laurent Bassin (lbassin) + - Roman Igoshin (masterro) + - Jeroen van den Nieuwenhuisen (nieuwenhuisen) + - Pierre Rebeilleau (pierrereb) + - Raphael de Almeida (raphaeldealmeida) - andrey1s - Abhoryo - Fabian Vogler (fabian) - Korvin Szanto - - soyuka + - Simon Ackermann - StΓ©phan Kochen + - Steven Dubois - Arjan Keeman + - Bálint Szekeres - Alaattin Kahramanlar (alaattin) - Sergey Zolotov (enleur) + - Nicole Cordes (ichhabrecht) + - Mark Beech (jaybizzle) - Maksim Kotlyar (makasim) + - Thibaut Arnoud (thibautarnoud) - Neil Ferreira - - Nathanael Noblet (gnat) - - Indra Gunawan (indragunawan) - Julie Hourcade (juliehde) - Dmitry Parnas (parnas) - - Paul LE CORRE + - Christian Weiske + - Maria Grazia Patteri + - SΓ©bastien COURJEAN + - Marko VuΕ‘ak + - Ismo Vuorinen - Tony Malzhacker - - Mathieu MARCHOIS + - Valentin + - Ali Tavafi + - Viet Pham + - Pchol + - divinity76 + - Yiorgos Kalligeros + - Arek Bochinski + - Rafael Tovar + - Amin Hosseini (aminh) + - Andreas Lutro (anlutro) + - DUPUCH (bdupuch) - Cyril Quintin (cyqui) + - Cyrille Bourgois (cyrilleb) - Gerard van Helden (drm) - Johnny Peck (johnnypeck) + - Geoffrey Monte (numerogeek) + - Martijn Boers (plebian) + - Plamen Mishev (pmishev) + - Sergii Dolgushev (serhey) + - Rein Baarsma (solidwebcode) + - Stephen Lewis (tehanomalousone) + - wicliff wolda (wickedone) + - Wim Molenberghs (wimm) + - Loic Chardonnet - Ivan Menshykov - David Romaní - Patrick Allaert + - Alexander Li (aweelex) - Gustavo Falco (gfalco) - Matt Robinson (inanimatt) + - Marcel Berteler + - sdkawata - Aleksey Podskrebyshev - Calin Mihai Pristavu + - Rainrider + - Oliver Eglseder + - zcodes + - JΓΆrn Lang - David MarΓ­n CarreΓ±o (davefx) - Fabien LUCAS (flucas2) - - Omar Yepez (oyepez003) + - Hidde Boomsma (hboomsma) + - Johan Wilfer (johanwilfer) + - Toby Griffiths (tog) + - Ashura + - Alessandra Lai + - Ernest Hymel + - Andrea Civita + - NicolΓ‘s Alonso - mwsaz - - Jelle Kapitein - - BenoΓt Bourgeois - - mantulo - - Stefan Kruppa - - mmokhi - - corphi + - LoginovIlya + - carlos-ea + - Olexandr Kalaidzhy + - JΓ©rΓ©my Benoist + - Ferran Vidal + - youssef saoubou + - elattariyassine + - Carlos Tasada + - zors1 + - Peter Simoncic + - lerminou + - Ahmad El-Bardan + - pdragun + - Noel Light-Hilary + - Emre YILMAZ + - Marcos Labad + - Antoine M + - Frank Jogeleit + - OndΕ™ej Frei + - Jenne van der Meer + - Storkeus + - Anton Zagorskii + - ging-dev + - zakaria-amm + - Geert De Deckere + - Agata + - dakur - grizlik + - florian-michael-mast + - Henry Snoek + - Vlad Dumitrache + - Alex Kalineskou - Derek ROTH + - Jeremy Benoist - Ben Johnson + - Jan Kramer - mweimerskirch + - robmro27 + - Vallel Blanco + - Bastien ClΓ©ment + - Benjamin Franzke + - Pavinthan + - Sylvain METAYER + - Benjamin Laugueux + - Ivo Valchev + - baron (bastien) + - BenoΓt Bourgeois (bierdok) + - Damien Harper (damien.harper) + - Dominik Pesch (dombn) - Dmytro Boiko (eagle) - Shin Ohno (ganchiku) - - Geert De Deckere (geertdd) - - Jacek JΔ™drzejewski (jacek.jedrzejewski) - - Jan Kramer (jankramer) + - Jaap van Otterdijk (jaapio) + - Kubicki Kamil (kubik) + - Simon Leblanc (leblanc_simon) + - Vladislav Nikolayev (luxemate) + - Martin Mandl (m2mtech) + - Maxime Pinot (maximepinot) + - Misha Klomp (mishaklomp) + - Jean-Baptiste GOMOND (mjbgo) + - Mikhail Prosalov (mprosalov) + - Ulrik Nielsen (mrbase) + - Artem (nexim) + - Nicolas ASSING (nicolasassing) + - Pierre GastΓ© (pierre_g) + - Pierre-Olivier Vares (povares) + - Ronny LΓ³pez (ronnylt) + - Julius (sakalys) - abdul malik ikhsan (samsonasik) - - Henry Snoek (snoek09) - - JΓ©rΓ©my M (th3mouk) - - Simone Di Maulo (toretto460) + - Dmitry (staratel) + - Tito Miguel Costa (titomiguelcosta) + - Wim Godden (wimg) + - Morgan Auchede - Christian Morgan - - Alexander Miehe (engerim) - - Morgan Auchede (mauchede) + - Alexander Miehe + - Simon (kosssi) - Sascha Dens (saschadens) + - Maxime Aknin (3m1x4m) + - Geordie + - Exploit.cz - Don Pinkster - Maksim Muruev - Emil Einarsson - - Thomas Landauer + - Jason Stephens - 243083df + - Tinjo SchΓΆni - Thibault Duplessis + - Quentin Favrie + - Matthias Derer + - vladyslavstartsev + - KΓ©vin - Marc Abramowitz + - michal - Martijn Evers + - Sjoerd Adema - Tony Tran - - Jacques Moati - - Balazs Csaba (balazscsaba2006) + - Evgeniy Koval + - Claas Augner + - Balazs Csaba - Bill Hance (billhance) - Douglas Reith (douglas_reith) - - Forfarle (forfarle) - Harry Walter (haswalt) + - Jeffrey Moelands (jeffreymoelands) + - Jacques MOATI (jmoati) - Johnson Page (jwpage) - Ruben Gonzalez (rubenruateltek) + - Ruslan Zavacky (ruslanzavacky) + - Stefano Cappellini (stefano_cappellini) - Michael Roterman (wtfzdotnet) - - Andrii Dembitskyi - Arno Geurts - AdΓ‘n Lobato (adanlobato) - Ian Jenkins (jenkoian) - Marcos GΓ³mez Vilches (markitosgv) - Matthew Davis (mdavis1982) - - Maks - - Antoine LA + - George Bateman + - misterx + - arend + - Vincent GodΓ© + - helmi + - Michael Steininger + - Nardberjean + - jersoe + - Eric Grimois + - Beno!t POLASZEK + - Armando + - Jens Schulze + - Olatunbosun Egberinde + - Knallcharge + - Michel Bardelmeijer + - Ikko Ashimine + - Erwin Dirks + - Markus RamΕ‘ak - den - - pawel-lewtak - - omerida + - George Dietrich + - jannick-holm + - Menno Holtkamp + - Ser5 + - Clemens Krack + - Bruno Baguette + - Alexis Lefebvre + - Michal Forbak + - Alexey Berezuev + - Pierrick Charron + - gechetspr + - brian978 + - Talha Zekeriya Durmuş + - bch36 + - Steve Hyde + - Ettore Del Negro + - dima-gr - GΓ‘bor TΓ³th + - Rodolfo Ruiz + - tsilefy + - Enrico + - JΓ©rΓ©mie Broutier + - Success Go + - Chris McGehee + - Benjamin Rosenberger + - Vladyslav Startsev + - Markus Klein + - Bruno Nogueira Nascimento Wowk + - Tomanhez + - satalaondrej + - Matthias DΓΆtsch + - jonmldr + - ouardisoft + - RTUnreal + - Richard Hodgson + - Sven Fabricius + - Bogdan + - Marco Pfeiffer - Daniel Cestari - Matt Janssen - - David Lima + - KΓ©vin Gonella + - Matteo Galli + - Ash014 + - Loenix + - Simon Frost + - Cantepie + - detinkin + - Harry Wiseman + - Steve Marvell + - Shyim + - sabruss + - Andrejs Leonovs + - Signor Pedro + - Matthias Larisch + - Maxime P + - Sean Templeton + - Yendric - StΓ©phane Delprat - - Brian Freytag (brianfreytag) - - Samuele Lilli (doncallisto) + - Matthias Meyer + - Temuri Takalandze (abgeo) + - Bernard van der Esch (adeptofvoltron) + - Benedict Massolle (bemas) + - Gerard Berengue Llobera (bere) + - Ronny (big-r) + - Anton (bonio) + - Alexandre Fiocre (demos77) + - Jordane VASPARD (elementaire) + - Erwan Nader (ernadoo) + - Faizan Akram Dar (faizanakram) + - Greg Szczotka (greg606) + - Ian Littman (iansltx) + - Nathan DIdier (icz) + - Ilia Lazarev (ilzrv) + - Arkadiusz Kondas (itcraftsmanpl) + - Joao Paulo V Martins (jpjoao) - Brunet Laurent (lbrunet) + - JΓ©rΓ©my (libertjeremy) - Florent Viel (luxifer) + - Maks 3w (maks3w) + - Mamikon Arakelyan (mamikon) + - Michiel Boeckaert (milio) + - Mike Milano (mmilano) + - Guillaume Lajarige (molkobain) + - Diego Aguiar (mollokhan) - Mikhail Yurasov (mym) - - LOUARDI Abdeltif (ouardisoft) + - PLAZANET Pierre (pedrotroller) + - Igor Tarasov (polosatus) - Robert Gruendler (pulse00) + - Ramazan APAYDIN (rapaydin) + - Babichev Maxim (rez1dent3) + - Christopher Georg (sky-chris) + - Francisco Alvarez (sormes) - Simon Terrien (sterrien) - - Tarmo LeppΓ€nen (tarlepp) + - Stephan Vierkant (svierkant) - BenoΓt Merlet (trompette) - - Koen Kuipers + - Aaron Piotrowski (trowski) + - Roman Tymoshyk (tymoshyk) + - Vincent MOULENE (vints24) - datibbaw - - Erik Saunier (snickers) + - Koen Kuipers (koku) + - Ryan Linnit + - Antoine Leblanc + - Andre Johnson + - MaPePeR + - Marco Pfeiffer + - Matthieu Bontemps + - Vivien - Rootie - - Kyle + - david-binda + - Alexandru NΔƒstase + - ddegentesh + - Anne-Julia Seitz + - Alexander Bauer (abauer) + - SΓ©bastien Santoro (dereckson) + - Gabriel Solomon (gabrielsolomon) - Daniel Alejandro Castro Arellano (lexcast) - - sensio - - Chris Tanaskoski + - Aleksandar Dimitrov (netbull) + - Gary Houbre (thegarious) + - Florent Morselli - Thomas Jarrand + - Baptiste Leduc (bleduc) - Antoine Bluchet (soyuka) - - Sebastien Morel (plopix) - Patrick Kaufmann - Anton Dyshkant + - Kirill Nesmeyanov (serafim) - Reece Fowell (reecefowell) - - MΓ‘tyΓ‘s Somfai (smatyas) - - stefan.r + - Muhammad Aakash - Guillaume Gammelin - ValΓ©rian Galliat - d-ph - Renan Taranto (renan-taranto) - - Thomas Talbot (ioni) - Rikijs Murgs - Uladzimir Tsykun - - Ben Ramsey (ramsey) - Amaury Leroux de Lens (amo__) - Christian Jul Jensen - - Alexandre GESLIN (alexandregeslin) + - Franck RANAIVO-HARISOA (franckranaivo) + - Alexandre GESLIN - The Whole Life to Learn - Mikkel Paulson - ergiegonzaga - - Farhad Safarov - - Alexis Lefebvre - Liverbool (liverbool) + - Dalibor KarloviΔ‡ - Sam Malone - - Phan Thanh Ha (haphan) + - Ha Phan (haphan) - Chris Jones (leek) - neghmurken + - stefan.r - xaav + - Jean-Christophe Cuvelier [Artack] - Mahmoud Mostafa (mahmoud) - Ahmed Abdou - Pieter - Michael Tibben - Billie Thompson - - Ganesh Chandrasekaran + - Ganesh Chandrasekaran (gxc4795) - Sander Marechal - Franz Wilding (killerpoke) - - ProgMiner + - Ferenczi Krisztian (fchris82) - Oleg Golovakhin (doc_tr) - - Joost van Driel - Icode4Food (icode4food) - RadosΕ‚aw Benkel - - EStyles (insidestyles) - - kevin.nadin + - Bert ter Heide (bertterheide) + - Kevin Nadin (kevinjhappy) - jean pasqualini (darkilliant) - Ross Motley (rossmotley) - ttomor - Mei Gwilym (meigwilym) - - Michael H. Arieli (excelwebzone) + - Michael H. Arieli + - Jitendra Adhikari (adhocore) - Tom Panier (neemzy) - Fred Cox - Luciano Mammino (loige) - fabios - Sander Coolen (scoolen) - Nicolas Le Goff (nlegoff) + - Anne-Sophie Bachelard + - Marvin Butkereit - Ben Oman + - Jack Worman (jworman) - Chris de Kok - - Andreas Kleemann + - Andreas Kleemann (andesk) + - Hubert Moreau (hmoreau) - Manuele Menozzi - - zairig imad (zairigimad) - Anton Babenko (antonbabenko) - Irmantas Ε iupΕ‘inskas (irmantas) + - Charles-Henri Bruyand - Danilo Silva - - Arnaud PETITPAS (apetitpa) + - Konstantin S. M. MΓΆllers (ksmmoellers) - Ken Stanley - Zachary Tong (polyfractal) - Ashura - Hryhorii Hrebiniuk - johnstevenson - - Antonio Pauletich (x-coder264) - hamza - dantleech - - Bastien DURAND (deamon) - - Xavier Leune - Rudy Onfroy - Tero AlΓ©n (tero) - - Stanislav Kocanda - DerManoMann - Guillaume Royer - Artem (digi) - boite - Silvio Ginter - MGDSoft + - joris - Vadim Tyukov (vatson) - David Wolter (davewww) - Sortex - chispita - Wojciech Sznapka - - Gavin Staniforth + - Gavin (gavin-markup) - Ksaveras Ε akys (xawiers) + - Shaun Simmons - Ariel J. Birnbaum - Danijel ObradoviΔ‡ - Pablo Borowicz - - Mathieu Santostefano - - Arjan Keeman + - OndΕ™ej Frei - MΓ‘ximo Cuadros (mcuadros) - - Lukas Mencl + - EXT - THERAGE Kevin - tamirvs - gauss - julien.galenski - - Christian Neff + - Florian Guimier + - Christian Neff (secondtruth) - Chris Tiearney - Oliver Hoff - Ole Râßner (basster) @@ -1092,71 +2083,77 @@ Symfony is the result of the work of many people who made the code better - Tom Houdmont - Per SandstrΓΆm (per) - Goran Juric - - Laurent Ghirardotti (laurentg) + - Laurent G. (laurentg) - Nicolas Macherey + - Asil Barkin Elik (asilelik) + - Bhujagendra Ishaya - Guido Donnari - - AKeeman (akeeman) - Mert Simsek (mrtsmsk0) - Lin Clark - Jeremy David (jeremy.david) - Jordi Rejas - Troy McCabe - Ville Mattila - - ilyes kooli - gr1ev0us + - LΓ©o VINCENT - mlazovla - Max Beutel + - Nathan Sepulveda - Antanas Arvasevicius - Pierre Dudoret + - Michal Trojanowski - Thomas + - Norbert Schultheisz - Maximilian Berghoff (electricmaxxx) - - nacho + - SOEDJEDE Felix (fsoedjede) - Piotr Antosik (antek88) - - Artem Lopata - - Patrick Reimers (preimers) + - Nacho Martin (nacmartin) - Sergey Novikov (s12v) + - ProgMiner - Marcos Quesada (marcos_quesada) - - Matthew Vickery (mattvick) + - Matthew (mattvick) - MARYNICH Mikhail (mmarynich-ext) - - Viktor Novikov (panzer_commander) + - Viktor Novikov (nowiko) - Paul Mitchum (paul-m) - Angel Koilov (po_taka) - Dan Finnie - Ken Marfilla (marfillaster) + - Max Grigorian (maxakawizard) + - allison guilhem - benatespina (benatespina) - Denis Kop - Jean-Guilhem Rouel (jean-gui) + - Ivan Yivoff + - EdgarPE - jfcixmedia - Dominic Tubach - - Nikita Konstantinov - Martijn Evers - - Vitaliy Ryaboy (vitaliy) - Benjamin Paap (benjaminpaap) - Christian - Denis Golubovskiy (bukashk0zzz) - - Sergii Smertin (nfx) + - Serge (nfx) - Mikkel Paulson - MichaΕ‚ Strzelecki - - Soner Sayakci - - hugofonseca (fonsecas72) + - Hugo Fonseca (fonsecas72) - Martynas Narbutas - - Toon Verwerft (veewee) - Bailey Parker - - Eddie Jaoude - Antanas Arvasevicius + - Kris Kelly + - Eddie Abou-Jaoude (eddiejaoude) - Haritz Iturbe (hizai) - Nerijus Arlauskas (nercury) - - SPolischook - Diego Sapriza - Joan Cruz - inspiran + - Alex Demchenko - Cristobal Dabed - Daniel Mecke (daniel_mecke) - Matteo Giachino (matteosister) - - Alex Demchenko (pilot) + - Serhii Polishchuk (spolischook) - Tadas Gliaubicas (tadcka) - Thanos Polymeneas (thanos) - Benoit Garret + - HellFirePvP - Maximilian Ruta (deltachaos) - Jakub Sacha - Olaf Klischat @@ -1164,36 +2161,30 @@ Symfony is the result of the work of many people who made the code better - Claude Dioudonnat - Jonathan Hedstrom - Peter Smeets (darkspartan) - - Jhonny Lidfors (jhonny) - Julien Bianchi (jubianchi) - Robert Meijers + - Tijs Verkoyen - James Sansbury - Marcin Chwedziak - hjkl - - Tony Cosentino (tony-co) - Dan Wilga - Andrew Tch - Alexander Cheprasov - Rodrigo DΓ­ez Villamuera (rodrigodiez) - - James Hudson - Stephen Clouse - e-ivanov - - Einenlum + - Yann Rabiller (einenlum) - Jochen Bayer (jocl) - Patrick Carlo-Hickman - Bruno MATEU - - Alex Bowers - Jeremy Bush - - wizhippo - - Mathias STRASSER (roukmoute) - Thomason, James - Gordienko Vladislav + - Ener-Getick - Viacheslav Sychov - - Alexandre Quercia (alquerci) - Helmut Hummel (helhum) - Matt Brunt - Carlos Ortega Huetos - - rpg600 - PΓ©ter Buri (burci) - kaiwa - Charles Sanquer (csanquer) @@ -1202,6 +2193,7 @@ Symfony is the result of the work of many people who made the code better - David Otton - Will Donohoe - peter + - Jeroen de Boer - JΓ©rΓ©my Jourdin (jjk801) - BRAMILLE SΓ©bastien (oktapodia) - Artem Kolesnikov (tyomo4ka) @@ -1210,27 +2202,30 @@ Symfony is the result of the work of many people who made the code better - Vladimir Luchaninov (luchaninov) - spdionis - rchoquet + - v.shevelev - gitlost - Taras Girnyk + - Sergio - Eduardo GarcΓ­a Sanz (coma) - - Sergio (deverad) - - James Gilliland - fduch (fduch) - David de Boer (ddeboer) - Eno Mullaraj (emullaraj) + - Stephan Vock (glaubinix) + - Guillem Fondin (guillemfondin) - Ryan Rogers + - Arnaud - Klaus Purer - - arnaud (arnooo999) - Gilles Doge (gido) - abulford - Philipp Kretzschmar - - antograssiot + - Jairo Pastor - Ilya Vertakov - Brooks Boyd - - johnillo + - Axel Venet - Roger Webb - Dmitriy Simushev - Pawel Smolinski + - John Espiritu (johnillo) - Oxan van Leeuwen - pkowalczyk - Soner Sayakci @@ -1238,15 +2233,17 @@ Symfony is the result of the work of many people who made the code better - Nicolas Fabre (nfabre) - Raul Rodriguez (raul782) - mshavliuk - - WybrenKoelmans - - Derek Lambert + - Jesper Skytte - MightyBranch - Kacper Gunia (cakper) + - Derek Lambert (dlambert) + - Mark Pedron (markpedron) - Peter Thompson (petert82) - error56 - Felicitus - - Krzysztof Przybyszewski - alexpozzi + - Krzysztof Przybyszewski (kprzybyszewski) + - BoullΓ© William (williamboulle) - Frederic Godfrin - Paul Matthews - Jakub Kisielewski @@ -1255,91 +2252,108 @@ Symfony is the result of the work of many people who made the code better - Alain Flaus (halundra) - tsufeki - Philipp Strube + - Petar ObradoviΔ‡ - Clement Herreman (clemherreman) - Dan Ionut Dumitriu (danionut90) + - Evgeny (disparity) - Vladislav Rastrusny (fractalizer) - Alexander Kurilo (kamazee) - - Nyro (nyro) + - nyro (nyro) - Marco - Marc Torres + - gndk - Alberto Aldegheri + - Dalibor KarloviΔ‡ + - Cyril VermandΓ© (cyve) - Dmitri Petmanson - heccjj - Alexandre Melard - - Jonathan (jls-esokia) + - AlbinoDrought - Jay Klehr - Sergey Yuferev - Tobias StΓΆckler - Mario Young + - martkop26 + - Sander Hagen - Ilia (aliance) - - Chris McCafferty (cilefen) + - cilefen (cilefen) - Mo Di (modi) - Pablo SchlΓ€pfer - - Gert de Pagter + - Robert Meijers + - Xavier RENAUDIN + - Christian Wahler (christian) - Jelte Steijaert (jelte) - David NΓ©grier (moufmouf) - Quique Porta (quiqueporta) - - stoccc + - Tobias Feijten (tobias93) - Andrea Quintino (dirk39) + - Andreas Heigl (heiglandreas) - Tomasz Szymczyk (karion) + - Peter Dietrich (xosofox) - Alex Vasilchenko - sez-open - - Xavier Coureau - ConneXNL - Aharon Perkel - matze - RubΓ©n Calvo (rubencm) - Abdul.Mohsen B. A. A - - BenoΓƒΒt Burnichon + - CΓ©dric Girard - pthompson - Malaney J. Hill + - Patryk KozΕ‚owski - Alexandre Pavy + - Tim Ward - Christian Flach (cmfcmf) - - CΓ©dric Girard (enk_) - Lars Ambrosius Wallenborn (larsborn) - Oriol Mangas Abellan (oriolman) - Sebastian GΓΆttschkes (sgoettschkes) - Tatsuya Tsuruoka - Ross Tuck + - omniError + - Zander Baldwin + - LΓ‘szlΓ³ GΓ–RΓ–G - KΓ©vin Gomez (kevin) + - Kevin van Sonsbeek (kevin_van_sonsbeek) - Mihai Nica (redecs) - - Soufian EZ-ZANTAR (soezz) - Andrei Igna - azine + - Wojciech ZimoΕ„ + - Pierre Tachoire - Dawid Sajdak - Ludek Stepan + - Mark van den Berg - Aaron Stephens (astephens) - Craig Menning (cmenning) - BalΓ‘zs BenyΓ³ (duplabe) - Erika Heidi Reinaldo (erikaheidi) - - Pierre Tachoire (krichprollsch) - Marc J. Schmidt (marcjs) + - Maximilian Beckers (maxbeckers) - Sebastian Schwarz + - karolsojko - Marco Jantke - Saem Ghani - - ClΓ©ment LEFEBVRE - - Conrad Kleinespel - Zacharias Luiten - Sebastian Utz - Adrien Gallou (agallou) - Maks Rafalko (bornfree) - - Karol SΓ³jko (karolsojko) - - sl_toto (sl_toto) + - Conrad Kleinespel (conradk) + - ClΓ©ment LEFEBVRE (nemoneph) - Walter Dal Mut (wdalmut) - abluchet + - PabloKowalczyk - Matthieu - Albin Kerouaton - Sébastien HOUZÉ - Jingyu Wang - steveYeah - - Samy Dindane (dinduks) + - Samy D (dinduks) - Keri Henare (kerihenare) - CΓ©dric Lahouste (rapotor) - Samuel Vogel (samuelvogel) - - Alexey Kopytko (sanmai) - Berat Doğan - Guillaume LECERF - Juanmi Rodriguez CerΓ³n + - twifty - Andy Raines - Anthony Ferrara - Geoffrey PΓ©cro (gpekz) @@ -1347,107 +2361,122 @@ Symfony is the result of the work of many people who made the code better - Klaas Cuvelier (kcuvelier) - Flavien Knuchel (knuch) - Mathieu TUDISCO (mathieutu) + - Peter Ward - markusu49 - Steve FrΓ©cinaux - Constantine Shtompel - Jules Lamur - Renato Mendes Figueiredo + - RaphaΓ«l Droz - Eric Stern - ShiraNai7 - Antal Áron (antalaron) - - Markus Fasselt (digilist) - VaΕ‘ek Purchart (vasek-purchart) - Janusz JabΕ‚oΕ„ski (yanoosh) - Fleuv - - Sandro Hopf - Łukasz Makuch - George Giannoulopoulos - - Alexander Pasichnick + - Alexander Pasichnik (alex_brizzz) - Luis Ramirez (luisdeimos) - Daniel Richter (richtermeister) + - Sandro Hopf (senaria) - ChrisC - - JL - - Ilya Biryukov + - jack.shpartko + - Willem Verspyck - Kim LaΓ― Trinh - Jason Desrosiers - m.chwedziak - Andreas FrΓΆmer + - Bikal Basnet - Philip Frank - Lance McNearney + - Illia Antypenko (aivus) + - Jelizaveta LemeΕ‘eva (broken_core) + - Dominik Ritter (dritter) + - Frank Neff (fneff) + - Ilya Biryukov (ibiryukov) + - Roma (memphys) - Giorgio Premi + - Krzysztof Pyrkosz - ncou - Ian Carroll - caponica - Daniel Kay (danielkay-cp) - Matt Daum (daum) - Alberto Pirovano (geezmo) + - Pascal Woerde (pascalwoerde) - Pete Mitchell (peterjmit) - Tom Corrigan (tomcorrigan) - Luis Galeas - Martin PΓ€rtel - FrΓ©dΓ©ric Bouchery (fbouchery) - Patrick Daley (padrig) - - Xavier Briand (xavierbriand) + - Phillip Look (plook) - Max Summe - - WedgeSama - - Felds Liscia + - Ema Panz - Chihiro Adachi (chihiro-adachi) - RaphaΓ«ll Roussel - Tadcka + - Abudarham Yuval - Beth Binkovitz - Gonzalo MΓ­guez - Romain Geissler - Adrien Moiruad - Tomaz Ahlin - - Philip Ardery - Nasim + - AnotherSymfonyUser (arderyp) - Marcus StΓΆhr (dafish) - Daniel GonzΓ‘lez Zaballos (dem3trio) - Emmanuel Vella (emmanuel.vella) - Guillaume BRETOU (guiguiboy) - - Dāvis ZālΔ«tis (k0d3r1s) + - Ibon Conesa (ibonkonesa) + - Yoann Chocteau (kezaweb) + - nuryagdy mustapayev (nueron) - Carsten Nielsen (phreaknerd) - - Roger Guasch (rogerguasch) - - Mathieu Rochette - Jay Severson - RenΓ© Kerner - Nathaniel Catchpole - Adrien Samson (adriensamson) - Samuel Gordalina (gordalina) - - Max Romanovsky (maxromanovsky) + - Maksym Romanowski (maxromanovsky) - Nicolas Eeckeloo (neeckeloo) - Andriy Prokopenko (sleepyboy) - - Mathieu Morlon + - Dariusz Ruminski - Daniel Tschinder - Arnaud CHASSEUX - Wojciech Gorczyca + - Mathieu Morlon (glutamatt) - RafaΕ‚ MuszyΕ„ski (rafmus90) - SΓ©bastien DecrΓͺme (sebdec) - Timothy Anido (xanido) + - acoulton - Mara Blaga - Rick Prent - skalpa - - Martin Eckhardt - Pieter Jordaan - - Damien Tournoud - - Jon Gotlin (jongotlin) + - Tournoud (damientournoud) - Michael Dowling (mtdowling) + - Arnaud POINTET (oipnet) - Karlos Presumido (oneko) - Tony Vermeiren (tony) - Thomas Counsell - BilgeXA - - r1pp3rj4ck - - phydevs - mmokhi - Robert Queck - Peter Bouwdewijn - - mlively + - Daniil Gentili + - Eduard Morcinek - Amine Matmati + - Kristen Gilden - caalholm - Nouhail AL FIDI (alfidi) - Fabian Steiner (fabstei) - - Felipy Tavares Amorim (felipyamorim) + - Felipy Amorim (felipyamorim) - Klaus Silveira (klaussilveira) + - Michael Lively (mlivelyjr) + - Abderrahim (phydev) + - Attila Bukor (r1pp3rj4ck) + - Thomas Boileau (tboileau) - Thomas Chmielowiec (chmielot) - Jānis Lukss - rkerner @@ -1456,12 +2485,12 @@ Symfony is the result of the work of many people who made the code better - Ergie Gonzaga - Matthew J Mucklo - AnrDaemon - - fdgdfg (psampaz) - - StΓ©phane Seng + - Charly Terrier (charlypoppins) + - Emre Akinci (emre) + - Rustam Bakeev (nommyde) + - psampaz (psampaz) - Maxwell Vandervelde - kaywalker - - Mike Meier - - Tim Jabs - Sebastian Ionescu - Robert Kopera - Pablo Ogando Ferreira @@ -1469,39 +2498,38 @@ Symfony is the result of the work of many people who made the code better - Simon Neidhold - Valentin VALCIU - Jeremiah VALERIE - - Julien Menth + - Patrik Patie Gmitter - Yannick Snobbert - Kevin Dew - James Cowgill - - 1ma (jautenim) + - sensio + - Julien Menth (cfjulien) + - Lyubomir Grozdanov (lubo13) - Nicolas Schwartz (nicoschwartz) - - Patrik Gmitter (patie) + - Tim Jabs (rubinum) + - StΓ©phane Seng (stephaneseng) - Jonathan Gough + - Benoit Leveque - Benjamin Bender - Jared Farrish + - Yohann Tilotti - karl.rixon - raplider - Konrad Mohrfeldt - Lance Chen - Ciaran McNulty (ciaranmcnulty) - Andrew (drew) - - Giso Stallenberg (gisostallenberg) - - kor3k kor3k (kor3k) + - j4nr6n (j4nr6n) - Stelian Mocanita (stelian) - - Justin (wackymole) - - Flavian (2much) - Gautier Deuette - - mike - Kirk Madera - Keith Ma 8000 ika - Mephistofeles - Hoffmann AndrΓ‘s + - CΓ©dric Anne - LubenZA - - Olivier - - Cyril PASCAL + - Flavian Sierk - Michael Bessolov - - pscheit - - Wybren Koelmans - Zdeněk Drahoš - Dan Harper - moldcraft @@ -1511,96 +2539,106 @@ Symfony is the result of the work of many people who made the code better - CΓ©sar SuΓ‘rez (csuarez) - Bjorn Twachtmann (dotbjorn) - Tobias Genberg (lorceroth) - - Luis TacΓ³n (lutacon) + - Michael Simonson (mikes) - Nicolas Badey (nico-b) + - Olivier Scherler (oscherler) - Shane Preece (shane) - Johannes Goslar - Geoff - georaldc - - Maarten de Boer - - Malte Wunsch - wusuopu + - Wouter de Wild + - Peter Potrowl - povilas - Gavin Staniforth - Alessandro Tagliapietra (alex88) - - Biji (biji) - - JΓ©rΓ΄me Tanghe (deuchnord) + - Nikita Starshinov (biji) - Alex Teterin (errogaht) - Gunnar Lium (gunnarlium) + - Malte Wunsch (maltewunsch) + - Simo Heinonen (simoheinonen) - Tiago Garcia (tiagojsag) - Artiom - Jakub Simon - Bouke Haarsma - mlievertz - Enrico Schultz - - Evert Harmeling - - mschop + - tpetry - Martin Eckhardt - natechicago + - Leonid Terentyev - Sergei Gorjunov - Jonathan Poston - Adrian Olek (adrianolek) - Jody Mickey (jwmickey) - PrzemysΕ‚aw Piechota (kibao) - - Leonid Terentyev (li0n) + - Martin Schophaus (m_schophaus_adcada) - Martynas Sudintas (martiis) + - Anton Sukhachev (mrsuh) + - Marcel Siegert - ryunosuke - - victoria - Francisco Facioni (fran6co) - Iwan van Staveren (istaveren) - Povilas S. (povilas) - Laurent Negre (raulnet) + - Victoria Quirante Ruiz (victoria) - Evrard Boulou - pborreli - Boris Betzholz - Eric Caron + - Arnau GonzΓ‘lez + - GurvanVgx - 2manypeople - Wing - Thomas Bibb + - Stefan Koopmanschap - Matt Farmer - catch - Alexandre Segura + - Asier Etxebeste - Josef Cech - Andrii Boiko - Harold Iedema - Ikhsan Agustian - - Arnau GonzΓ‘lez (arnaugm) + - Benoit LΓ©vΓͺque (benoit_leveque) - Simon Bouland (bouland) + - Jakub Janata (janatjak) - JibΓ© Barth (jibbarth) - Matthew Foster (mfoster) - Reyo Stallenberg (reyostallenberg) - Paul Seiffert (seiffert) - Vasily Khayrulin (sirian) - - Stefan Koopmanschap (skoop) - Stas Soroka (stasyan) - Stefan HΓΌsges (tronsha) - Jake Bishop (yakobeyak) - Dan Blows - Matt Wells - - Sander van der Vlugt - Nicolas Appriou - stloyd - Andreas - Chris Tickner - - BoShurik - Andrew Coulton - Ulugbek Miniyarov - Jeremy Benoist + - sdrewergutland - Michal Gebauer + - Phil Davis - Gleb Sidora - David Stone - Jovan Perovic (jperovic) - Pablo Maria Martelletti (pmartelletti) + - Sander van der Vlugt (stranding) - Yassine Guedidi (yguedidi) + - Florian Bogey - Waqas Ahmed - Bert Hekman - Luis MuΓ±oz - Matthew Donadio + - Kris Buist - Houziaux mike - Phobetor - - Andreas - Markus - - Daniel Gorgan + - Janusz Mocek - Thomas Chmielowiec - shdev - Andrey Ryaguzov @@ -1609,47 +2647,51 @@ Symfony is the result of the work of many people who made the code better - Manatsawin Hanmongkolchai - Gunther Konig - Mickael GOETZ + - Tobias Speicher + - Jesper Noordsij + - DerStoffel - Maciej Schmidt - - Dennis VΓ¦versted + - tatankat - nuncanada - - flack + - Thierry Marianne - FrantiΕ‘ek Bereň - - Kamil Madejski - Jeremiah VALERIE - Mike Francis + - Nil Borodulia + - Almog Baku (almogbaku) + - Benjamin Schultz (bschultz) - Gerd Christian Kunze (derdu) - - Christoph Nissle (derstoffel) - Ionel Scutelnicu (ionelscutelnicu) + - Kamil Madejski (kmadejski) - Nicolas TallefourtanΓ© (nicolab) - Botond Dani (picur) - - Thierry Marianne (thierrymarianne) + - Radek Wionczek (rwionczek) - Nick Stemerdink - David Stone - - jjanvier - - Julius Beckmann - - loru88 + - Grayson Koonce + - Wissame MEKHILEF - Romain Dorgueil - Christopher Parotat - Dennis Haarbrink - - me_shaon - 蝦米 - - Grayson Koonce (breerly) + - Julius Beckmann (h4cc) - Andrey Helldar (helldar) + - Julien JANVIER (jjanvier) - Karim Cassam ChenaΓ― (ka) - - Maksym Slesarenko (maksym_slesarenko) + - Lorenzo Adinolfi (loru88) + - Ahmed Shamim Hassan (me_shaon) - Michal Kurzeja (mkurzeja) - Nicolas Bastien (nicolas_bastien) - Nikola Svitlica (thecelavi) - - Denis (yethee) - Andrew Zhilin (zhil) - Sjors Ottjes - azjezz - Andy Stanberry - Felix Marezki - Normunds - - Luiz β€œFelds” Liscia + - Walter Doekes - Thomas Rothe - - Martin + - Troy Crawford - nietonfir - alefranz - David Barratt @@ -1657,13 +2699,13 @@ Symfony is the result of the work of many people who made the code better - Pavel.Batanov - avi123 - Pavel Prischepa + - Philip DahlstrΓΈm + - Pierre Schmitz - alsar - downace - AarΓ³n Nieves FernΓ‘ndez - - Mike Meier - - Vilius GrigaliΕ«nas + - Ph3nol - Kirill Saksin - - Julien Pauli - Koalabaerchen - michalmarcinkowski - Warwick @@ -1675,124 +2717,128 @@ Symfony is the result of the work of many people who made the code better - efeen - Nicolas Pion - Muhammed Akbulut + - Xesau - Aaron Somi - - Karoly Gossler (connorhu) - MichaΕ‚ DΔ…browski (defrag) - - Konstantin Grachev (grachevko) - Simone Fumagalli (hpatoio) - Brian Graham (incognito) - Kevin Vergauwen (innocenzo) - Alessio Baglio (ioalessio) - - Jan van Thoor (janvt) - Johannes MΓΌller (johmue) - Jordi Llonch (jordillonch) + - julien_tempo1 (julien_tempo1) - Nicholas Ruunu (nicholasruunu) - - CΓ©dric Dugat (ph3nol) - - Philip DahlstrΓΈm (phidah) - Milos Colakovic (project2481) - RΓ©nald Casagraude (rcasagraude) - Robin Duval (robin-duval) - - Grinbergs Reinis (shima5) + - Mohammad Ali Sarbanha (sarbanha) + - Steeve Titeca (stiteca) - Artem Lopata (bumz) - alex - - Nicole Cordes - - Nicolas PHILIPPE - Roman Orlov + - Andreas Allacher - VolCh - Alexey Popkov - Gijs Kunze - Artyom Protaskin - Nathanael d. Noblet + - Yurun - helmer - ged15 + - Simon Asika - Daan van Renterghem - - Nicole Cordes - - Martin Kirilov + - Boudry Julien - amcastror - - Alexander Li (aweelex) - Bram Van der Sype (brammm) - Guile (guile) - Julien Moulin (lizjulien) - Raito Akehanareru (raito) - Mauro Foti (skler) - Yannick Warnier (ywarnier) + - JΓΆrn Lang - Kevin Decherf + - Paul LE CORRE - Jason Woods - - Oleg Andreyev - klemens - dened + - jpauli - Dmitry Korotovsky - - mcorteel - Michael van Tricht - ReScO - Tim Strehle - Sam Ward + - Hans N. Hjort - Walther Lalk - Adam - Ivo - SΓΆren Bernstein - devel - taiiiraaa - - Trevor Suarez - gedrox - Alan Bondarchuk - - Joe Bennett - dropfen - Andrey Chernykh - Edvinas Klovas - Drew Butler - Peter Breuls + - Kevin EMO - Chansig - Tischoi - Andreas Hasenack - J Bruni - - Fritz Michael Gschwantner - Alexey Prilipko - - Dmitriy Fedorenko - vlakoff - - bertillon - thib92 - Rudolf RatusiΕ„ski - Bertalan Attila - AmsTaFF (amstaff) - Simon MΓΌller (boscho) - Yannick Bensacq (cibou) - - Damien (damien_vauchel) + - Damien Vauchel (damien_vauchel) + - Dmitrii Fedorenko (dmifedorenko) - FrΓ©dΓ©ric G. Marand (fgm) - Freek Van der Herten (freekmurze) - Luca Genuzio (genuzio) - - Hans Nilsson (hansnilsson) - Andrew Marcinkevičius (ifdattic) - Ioana Hazsda (ioana-hazsda) - Jan Marek (janmarek) - Mark de Haan (markdehaan) + - Maxime Corteel (mcorteel) - Dan Patrick (mdpatrick) + - Mathieu MARCHOIS (mmar) - Pedro MagalhΓ£es (pmmaga) - Rares Vlaseanu (raresvla) + - Trevor N. Suarez (rican7) + - ClΓ©ment Bertillon (skigun) - tante kinast (tante) - - Ahmed Hannachi (tiecoders) + - Adam RANDI (tiecoders) - Vincent LEFORT (vlefort) - Walid BOUGHDIRI (walidboughdiri) - Darryl Hein (xmmedia) - - Sadicov Vladimir (xtech) - - Kevin EMO (zarcox) + - Vladimir Sadicov (xtech) + - Peter van Dommelen + - Tim van Densen - Andrzej - Alexander Zogheb - RΓ©mi Blaise - Nicolas SΓ©verin + - Houssem - Joel Marcey + - zolikonta - David Christmann - root - pf + - Zoli Konta - Vincent Chalnot - - James Hudson + - Roeland Jago Douma + - Patrizio Bekerle - Tom Maguire - Mateusz Lerczak - Richard Quadling - David Zuelke - Adrian - - Oleg Andreyev - neFAST + - Peter Gribanov - Pierre Rineau - Florian Morello - Maxim Lovchikov @@ -1801,58 +2847,60 @@ Symfony is the result of the work of many people who made the code better - Ari Pringle (apringle) - Dan Ordille (dordille) - Jan Eichhorn (exeu) + - Georg Ringer (georgringer) - GrΓ©gory Pelletier (ip512) - John Nickell (jrnickell) - Martin Mayer (martin) - Grzegorz Łukaszewicz (newicz) + - Omar Yepez (oyepez003) - Jonny Schmid (schmidjon) - Götz Gottwald - - Veres Lajos + - Christoph Krapp - Nick Chiu - - grifx - Robert Campbell - Matt Lehner - Helmut Januschka - Hein Zaw Htetβ„’ - Ruben Kruiswijk - Cosmin-Romeo TANASE - - Julien Maulny - Michael J - Joseph Maarek - Alexander Menk - Alex Pods - - hadriengem - timaschew + - Jelle Kapitein - Jochen Mandl - Marin Nicolae + - Gerrit Addiks + - Albert Prat - Alessandro Loffredo - Ian Phillips - - Marco Lipparini + - Remi Collet - Haritz - Matthieu Prat - - Ion Bazan - - Grummfy + - Brieuc Thomas + - mantulo - Paul Le Corre - Filipe Guerra - Jean Ragouin - Gerben Wijnja - Rowan Manning + - qsz - Per Modin - David Windell - Gabriel Birke - - skafandri - Derek Bonner - martijn + - annesosensio + - NothingWeAre + - goabonga - Alan Chen - - insidestyles - Maerlyn - Even André Fiskvik - - АлСксандр Π›ΠΈ - - Arjan Keeman - Erik van Wingerden - Valouleloup - - Dane Powell - Alexis MARQUIS + - Matheus Gontijo - Gerrit Drost - Linnaea Von Lavia - Simon MΓΆnch @@ -1865,21 +2913,17 @@ Symfony is the result of the work of many people who made the code better - Juan M MartΓ­nez - Gilles Gauthier - ddebree - - Kuba WerΕ‚os - Gyula Szucs - Tomas Liubinas - - Alex - Jan Hort - Klaas Naaijkens - - Daniel GonzΓ‘lez CerviΓ±o - RafaΕ‚ - - Achilles Kaloeridis (achilles) - Adria Lopez (adlpz) - Aaron Scherer (aequasi) + - Alexandre Jardin (alexandre.jardin) + - Bart Brouwer (bartbrouwer) - Rosio (ben-rosio) - Simon Paarlberg (blamh) - - Jeroen Thora (bolle) - - Brieuc THOMAS (brieucthomas) - Masao Maeda (brtriver) - Darius Leskauskas (darles) - david perez (davidpv) @@ -1889,11 +2933,14 @@ Symfony is the result of the work of many people who made the code better - TomΓ‘Ε‘ PolΓ­vka (draczris) - Dennis Smink (dsmink) - Franz Liedke (franzliedke) + - Alex (garrett) - Gaylord Poillon (gaylord_p) - - Christophe BECKER (goabonga) - gondo (gondo) + - Joris Garonian (grifx) + - Grummfy (grummfy) + - Hadrien Cren (hcren) - Gusakov Nikita (hell0w0rd) - - Osman ÜngΓΌr (import) + - Oz (import) - Javier NΓΊΓ±ez Berrocoso (javiernuber) - Jelle Bekker (jbekker) - Giovanni Albero (johntree) @@ -1911,26 +2958,28 @@ Symfony is the result of the work of many people who made the code better - Dmitriy Tkachenko (neka) - Cayetano Soriano Gallego (neoshadybeat) - Olivier Laviale (olvlvl) - - Ondrej Machulda (ondram) - Pablo Monterde Perez (plebs) - Jimmy Leger (redpanda) + - Mokhtar Tlili (sf-djuba) - Marcin Szepczynski (szepczynski) + - Simone Di Maulo (toretto460) - Cyrille Jouineau (tuxosaurus) + - Lajos Veres (vlajos) - Vladimir Chernyshev (volch) - Yorkie Chadwick (yorkie76) + - Pavel Barton - GuillaumeVerdon - - Philipp Keck - - Angel Fernando Quiroz Campos - - Ondrej Mirtes + - ureimers - akimsko - Youpie - srsbiz - Taylan Kasap - Michael Orlitzky - Nicolas A. BΓ©rard-Nault + - Francois Martin - Saem Ghani - Stefan Oderbolz - - Curtis + - TamΓ‘s Szigeti - Gabriel Moreira - Alexey Popkov - ChS @@ -1943,87 +2992,88 @@ Symfony is the result of the work of many people who made the code better - Lars Moelleken - dasmfm - Mathias Geat + - Angel Fernando Quiroz Campos (angelfqc) - Arnaud Buathier (arnapou) + - Curtis (ccorliss) - chesteroni (chesteroni) - Mauricio Lopez (diaspar) - HADJEDJ Vincent (hadjedjvincent) - Daniele Cesarini (ijanki) - Ismail Asci (ismailasci) - - Simon CONSTANS (kosssi) - - Kristof Van Cauwenbergh (kristofvc) - - Dennis Langen (nijusan) + - OndΕ™ej Mirtes (mirtes) - Paulius Jarmalavičius (pjarmalavicius) - - Ramon Henrique Ornelas (ramonornela) - - Ricardo de Vries (ricknox) - - Markus S. (staabm) + - Ramon Ornelas (ramonornela) + - Ricardo de Vries (ricardodevries) - Thomas Dutrion (theocrite) - Till Klampaeckel (till) - Tobias Weinert (tweini) - - Ulf Reimers (ureimers) - Wotre - goohib - - Chi-teck - Tom Counsell + - Sepehr Lajevardi - Xavier HAUSHERR - - Ron GΓ€hler - Edwin Hageman - Mantas UrnieΕΎa - temperatur + - Paul Andrieux + - Sezil - Cas - - Dusan Kasan + - ghazy ben ahmed - Karolis - Myke79 - Brian Debuire - Piers Warmers - - Guilliam Xavier - Sylvain Lorinet - klyk50 - - Andreas Lutro - jc - BenjaminBeck - Aurelijus RoΕΎΔ—nas - Jordan Hoff - znerol - Christian Eikermann - - Kai Eichinger + - Sergei Shitikov - Antonio Angelino + - Pavel Golovin - Matt Fields - - Niklas Keller - Andras Debreczeni - Vladimir Sazhin - Tomas Kmieliauskas - Billie Thompson - lol768 - jamogon + - Antoine LA - Vyacheslav Slinko + - Benjamin Laugueux - Jakub ChΓ‘bek + - William Pinaud (DocFX) - Johannes - JΓΆrg RΓΌhl - wesleyh - - sergey - Michael Hudson-Doyle - Daniel Bannert - Karim Miladi - Michael Genereux - patrick-mcdougle + - Tyler Stroud - Dariusz Czech - Jack Wright - MrNicodemuz - Anonymous User + - demeritcowboy - PaweΕ‚ Tomulik - Eric J. Duran + - Blackfelix - Alexandru Bucur - cmfcmf - Drew Butler + - pawel-lewtak - Steve MΓΌller + - omerida - Andras Ratz - andreabreu98 - Michael Schneider - - CΓ©dric Bertolini - n-aleha - Anatol Belski - - Anderson MΓΌller - - ΕžΙ™hriyar Δ°manov - Alexis BOYER - Kaipi Yann - adam-mospan @@ -2031,24 +3081,30 @@ Symfony is the result of the work of many people who made the code better - Guillaume Aveline - Adrian Philipp - James Michael DuPont + - Markus Tacker - Kasperki - Tammy D - - Daniel STANCU + - Adrien Foulon - Ryan Rud - Ondrej SlintΓ‘k - vlechemin - Brian Corrigan - Ladislav TΓ‘nczos + - Brian Freytag - Skorney - Lucas Matte - fmarchalemisys + - MGatner - mieszko4 - Steve Preston + - ibasaw - Wojciech Skorodecki - Kevin Frantz - Neophy7e - bokonet - Arrilot + - andrey-tech + - Shaun Simmons - Markus Staab - Pierre-Louis LAUNAY - djama @@ -2061,13 +3117,13 @@ Symfony is the result of the work of many people who made the code better - Adam Klvač - Yevgen Kovalienia - Lebnik - - nsbx - Shude - OndΕ™ej FΓΌhrer - Sema - - Elan RuusamΓ€e - - Jon Dufresne + - Ayke Halder - Thorsten Hallwas + - Brian Freytag + - Alex Nostadt - Michael Squires - Egor Gorbachev - Derek Stephen McLean @@ -2075,13 +3131,14 @@ Symfony is the result of the work of many people who made the code better - zorn - Yuriy Potemkin - Emilie Lorenzo + - prudhomme victor - enomotodev - - Edvin Hultberg + - Vincent - Benjamin Long - Ben Miller - Peter Gribanov + - Bart Ruysseveldt - kwiateusz - - jspee - Ilya Bulakh - David Soria Parra - Sergiy Sokolenko @@ -2091,41 +3148,44 @@ Symfony is the result of the work of many people who made the code better - Yurii K - Richard TrebichavskΓ½ - g123456789l + - Mark Ogilvie - Jonathan Vollebregt + - Vladimir Vasilev - oscartv - DanSync - Peter Zwosta + - Michal ČihaΕ™ - parhs - Diego Campoy - - TeLiXj - Oncle Tom - Sam Anthony - Christian Stocker - Oussama Elgoumri + - David Lima - Dawid Nowak - Lesnykh Ilia - darnel - - Karolis DauΕΎickas - Nicolas - Sergio Santoro - tirnanog06 + - Alfonso FernΓ‘ndez GarcΓ­a - phc - Π”ΠΌΠΈΡ‚Ρ€ΠΈΠΉ ΠŸΠ°Ρ†ΡƒΡ€Π° - - ilyes kooli - MichaΓ«l VEROUX - Julia - Lin Lu - arduanov - sualko - - Bilge + - Fabien + - Martin Komischke - ADmad - Nicolas Roudaire - - Alfonso (afgar) + - Abdouni Karim (abdounikarim) - Andreas Forsblom (aforsblo) - Alex Olmos (alexolmos) + - Cedric BERTOLINI (alsciende) - Antonio Mansilla (amansilla) - Robin Kanters (anddarerobin) - - Andrii Popov (andrii-popov) - Juan Ases GarcΓ­a (ases) - Siragusa (asiragusa) - Daniel Basten (axhm3a) @@ -2135,11 +3195,12 @@ Symfony is the result of the work of many people who made the code better - Choong Wei Tjeng (choonge) - Kousuke Ebihara (co3k) - LoΓ―c Vernet (coil) - - Christian Gripp (core23) - - Christoph Schaefer (cvschaefer) + - Christoph Vincent Schaefer (cvschaefer) - Damon Jones (damon__jones) + - David Courtey (david-crty) - Łukasz Giza (destroyer) - Daniel Londero (dlondero) + - DuΕ‘an Kasan (dudo1904) - Sebastian Landwehr (dword123) - Adel ELHAIBA (eadel) - DamiΓ‘n Nohales (eagleoneraptor) @@ -2149,11 +3210,8 @@ Symfony is the result of the work of many people who made the code better - Sorin Gitlan (forapathy) - Yohan Giarelli (frequence-web) - Gerry Vandermaesen (gerryvdm) - - Ghazy Ben Ahmed (ghazy) - - Arash Tabriziyan (ghost098) - - ibasaw (ibasaw) + - Arash Tabrizian (ghost098) - Vladislav Krupenkin (ideea) - - Ilija Tovilo (ilijatovilo) - Peter Orosz (ill_logical) - Imangazaliev Muhammad (imangazaliev) - j0k (j0k) @@ -2167,6 +3225,7 @@ Symfony is the result of the work of many people who made the code better - JuntaTom (juntatom) - Julien Manganne (juuuuuu) - Ismail Faizi (kanafghan) + - Karolis DauΕΎickas (kdauzickas) - SΓ©bastien Armand (khepin) - Pierre-Chanel Gauthier (kmecnin) - Krzysztof MenΕΌyk (krymen) @@ -2174,102 +3233,95 @@ Symfony is the result of the work of many people who made the code better - Laurent Bachelier (laurentb) - LuΓ­s Cobucci (lcobucci) - Mehdi Achour (machour) - - Matthieu Mota (matthieumota) - - Matthieu Moquet (mattketmo) + - Matt Ketmo (mattketmo) - Moritz Borgmann (mborgmann) - - Michal ČihaΕ™ (mcihar) - Matt Drollette (mdrollette) - Adam Monsen (meonkeys) - Ala Eddine Khefifi (nayzo) - emilienbouard (neime) - Nicholas Byfleet (nickbyfleet) - - Marco Petersen (ocrampete16) + - Nicolas Bondoux (nsbx) + - Cedric Kastner (nurtext) - ollie harridge (ollietb) - - Dimitri Gritsajuk (ottaviano) - - Paul Andrieux (paulandrieux) - - PaweΕ‚ Szczepanek (pauluz) + - Pawel Szczepanek (pauluz) - Philippe Degeeter (pdegeeter) - Christian LΓ³pez EspΓ­nola (penyaskito) - Petr JaroΕ‘ (petajaros) - Philipp Hoffmann (philipphoffmann) - Alex Carol (picard89) - Daniel Perez Pinazo (pitiflautico) - - Phil Taylor (prazgod) - - Maxim Pustynnikov (pustynnikov) - - Ralf Kuehnel (ralfkuehnel) - - Brayden Williams (redstar504) + - Maksym Pustynnikov (pustynnikov) + - Ralf KΓΌhnel (ralfkuehnel) - Rich Sage (richsage) - - Bart Ruysseveldt (ruyss) - scourgen hung (scourgen) - Sebastian Busch (sebu) - - Sepehr Lajevardi (sepehr) + - Sergey Stavichenko (sergey_stavichenko) - AndrΓ© Filipe GonΓ§alves Neves (seven) - Bruno Ziegler (sfcoder) - Andrea Giuliano (shark) + - ΕžΙ™hriyar Δ°manov (shehriyari) - Thomas Baumgartner (shoplifter) - Schuyler Jager (sjager) - Volker (skydiablo) - Julien Sanchez (sumbobyboys) + - Ron GΓ€hler (t-ronx) - Guillermo Gisinger (t3chn0r) - - Markus Tacker (tacker) - Tom Newby (tomnewbyau) - Andrew Clark (tqt_andrew_clark) - David Lumaye (tux1124) - - Tyler Stroud (tystr) - Moritz Kraft (userfriendly) - VΓ­ctor Mateo (victormateo) - - Vincent (vincent1870) - - David Herrmann (vworldat) + - David GrΓΌner (vworldat) - Eugene Babushkin (warl) - Wouter Sioen (wouter_sioen) - Xavier Amado (xamado) - Jesper SΓΈndergaard Pedersen (zerrvox) - Florent Cailhol - szymek + - Konrad - Kovacs Nicolas - craigmarvelley - Stano Turza - - simpson - drublic - Andreas Streichardt + - Alexandre Segura - Pascal Hofmann - smokeybear87 - Gustavo Adrian - damaya - Kevin Weber - - Ben Scott - Dionysis Arvanitis - Sergey Fedotov - Konstantin Scheumann - Michael - fh-github@fholzhauer.de + - rogamoore - AbdElKader Bouadjadja - DSeemiller - Jan Emrich - Mark Topper + - Romain - Xavier REN - - Zander Baldwin - - Philipp Scheit - max - Ahmad Mayahi (ahmadmayahi) - Mohamed Karnichi (amiral) - Andrew Carter (andrewcarteruk) - Adam Elsodaney (archfizz) - - Pablo Lozano (arkadis) - GregΓ³rio Bonfante Borba (bonfante) - Bogdan Rancichi (devck) - Daniel Kolvik (dkvk) - Marc Lemay (flug) - Henne Van Och (hennevo) - Jeroen De Dauw (jeroendedauw) - - Jonathan Scheiber (jmsche) - Maxime COLIN (maximecolin) - Muharrem Demirci (mdemirci) - Evgeny Z (meze) - - Nicolas de MarquΓ© (nicola) + - Pierre-Henry Soria 🌴 (pierrehenry) - Pierre Geyer (ptheg) - Thomas BERTRAND (sevrahk) + - Vladislav (simpson) - Matej Ε½ilΓ‘k (teo_sk) - Vladislav Vlastovskiy (vlastv) - RENAUDIN Xavier (xorrox) - Yannick Vanhaeren (yvh) + - Zan Baldwin (zanderbaldwin) diff --git a/LICENSE b/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/LICENSE +++ b/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 5796b1acd7ceb..bddcd21f97762 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@

-[Symfony][1] is a **PHP framework** for web applications and a set of reusable +[Symfony][1] is a **PHP framework** for web and console applications and a set of reusable **PHP components**. Symfony is used by thousands of web applications (including BlaBlaCar.com and Spotify.com) and most of the [popular PHP projects][2] (including Drupal and Magento). @@ -29,7 +29,7 @@ Community * [Join the Symfony Community][11] and meet other members at the [Symfony events][12]. * [Get Symfony support][13] on Stack Overflow, Slack, IRC, etc. * Follow us on [GitHub][14], [Twitter][15] and [Facebook][16]. -* Read our [Code of Conduct][24] and meet the [CARE Team][25] +* Read our [Code of Conduct][24] and meet the [CARE Team][25]. Contributing ------------ @@ -53,7 +53,7 @@ Symfony development is sponsored by [SensioLabs][21], led by the [2]: https://symfony.com/projects [3]: https://symfony.com/doc/current/reference/requirements.html [4]: https://symfony.com/doc/current/setup.html -[5]: http://semver.org +[5]: https://semver.org [6]: https://symfony.com/doc/current/contributing/community/releases.html [7]: https://symfony.com/doc/current/page_creation.html [8]: https://symfony.com/doc/current/index.html diff --git a/UPGRADE-4.0.md b/UPGRADE-4.0.md index 30a3805bc8a50..a7e49d469c502 100644 --- a/UPGRADE-4.0.md +++ b/UPGRADE-4.0.md @@ -26,8 +26,8 @@ file and directory structure of your application: Then, upgrade the contents of your console script and your front controller: -* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/3.3/bin/console -* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/3.3/public/index.php +* `bin/console`: https://github.com/symfony/recipes/blob/master/symfony/console/4.4/bin/console +* `public/index.php`: https://github.com/symfony/recipes/blob/master/symfony/framework-bundle/4.4/public/index.php Lastly, read the following article to add Symfony Flex to your application and upgrade the configuration files: https://symfony.com/doc/current/setup/flex.html @@ -250,9 +250,9 @@ DependencyInjection DoctrineBridge -------------- -* The `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandler` and - `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandlerSchema` have been removed. Use - `Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` instead. + * The `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandler` and + `Symfony\Bridge\Doctrine\HttpFoundation\DbalSessionHandlerSchema` have been removed. Use + `Symfony\Component\HttpFoundation\Session\Storage\Handler\PdoSessionHandler` instead. EventDispatcher --------------- @@ -287,9 +287,9 @@ Finder Form ---- -* The values of the `FormEvents::*` constants have been updated to match the - constant names. You should only update your application if you relied on the - constant values instead of their names. + * The values of the `FormEvents::*` constants have been updated to match the + constant names. You should only update your application if you relied on the + constant values instead of their names. * The `choices_as_values` option of the `ChoiceType` has been removed. @@ -817,8 +817,8 @@ Translation TwigBundle ---------- -* The `ContainerAwareRuntimeLoader` class has been removed. Use the - Twig `Twig_ContainerRuntimeLoader` class instead. + * The `ContainerAwareRuntimeLoader` class has been removed. Use the + Twig `Twig_ContainerRuntimeLoader` class instead. * Removed `DebugCommand` in favor of `Symfony\Bridge\Twig\Command\DebugCommand`. @@ -870,7 +870,6 @@ TwigBridge * Removed `LintCommand::set/getTwigEnvironment`. Pass an instance of `Twig\Environment` as first argument of the constructor instead. - Validator --------- @@ -924,7 +923,7 @@ Validator VarDumper --------- - * The `VarDumperTestTrait::assertDumpEquals()` method expects a 3rd `$context = null` + * The `VarDumperTestTrait::assertDumpEquals()` method expects a 3rd `$filter = 0` argument and moves `$message = ''` argument at 4th position. Before: @@ -939,7 +938,7 @@ VarDumper VarDumperTestTrait::assertDumpEquals($dump, $data, $filter = 0, $message = ''); ``` - * The `VarDumperTestTrait::assertDumpMatchesFormat()` method expects a 3rd `$context = null` + * The `VarDumperTestTrait::assertDumpMatchesFormat()` method expects a 3rd `$filter = 0` argument and moves `$message = ''` argument at 4th position. Before: diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index 3b1f238fa6f84..8f7cc54411aa9 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -192,15 +192,15 @@ HttpKernel * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been deprecated * The `KernelInterface::getName()` and the `kernel.name` parameter have been deprecated - * Deprecated the first and second constructor argument of `ConfigDataCollector` - * Deprecated `ConfigDataCollector::getApplicationName()` + * Deprecated the first and second constructor argument of `ConfigDataCollector` + * Deprecated `ConfigDataCollector::getApplicationName()` * Deprecated `ConfigDataCollector::getApplicationVersion()` Messenger --------- * The `MiddlewareInterface::handle()` and `SenderInterface::send()` methods must now return an `Envelope` instance. - * The return value of handlers isn't forwarded anymore by middleware and buses. + * The return value of handlers isn't forwarded anymore by middleware and buses. If you used to return a value, e.g in query bus handlers, you can either: - get the result from the `HandledStamp` in the envelope returned by the bus. - use the `HandleTrait` to leverage a message bus, expecting a single, synchronous message handling and returning its result. @@ -214,15 +214,15 @@ Messenger $query->setResult($yourResult); ``` * The `EnvelopeAwareInterface` was removed and the `MiddlewareInterface::handle()` method now requires an `Envelope` object - as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. + as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. + If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. If you created your own middleware, you must change the signature to always expect an `Envelope`. * The `MiddlewareInterface::handle()` second argument (`callable $next`) has changed in favor of a `StackInterface` instance. - When using built-in middleware with the provided `MessageBus`, you will not have to do anything. - If you use your own `MessageBusInterface` implementation, you can use the `StackMiddleware` implementation. + When using built-in middleware with the provided `MessageBus`, you will not have to do anything. + If you use your own `MessageBusInterface` implementation, you can use the `StackMiddleware` implementation. If you created your own middleware, you must change the signature to always expect an `StackInterface` instance and call `$stack->next()->handle($envelope, $stack)` instead of `$next` to call the next middleware: - + Before: ```php public function handle($message, callable $next): Envelope @@ -230,7 +230,7 @@ Messenger // do something before $message = $next($message); // do something after - + return $message; } ``` @@ -242,7 +242,7 @@ Messenger // do something before $envelope = $stack->next()->handle($envelope, $stack); // do something after - + return $envelope; } ``` @@ -251,7 +251,7 @@ Messenger respectively `ReceivedStamp`, `ValidationStamp`, `SerializerStamp` and moved to the `Stamp` namespace. * `AllowNoHandlerMiddleware` has been removed in favor of a new constructor argument on `HandleMessageMiddleware` * The `ConsumeMessagesCommand` class now takes an instance of `Psr\Container\ContainerInterface` - as first constructor argument, i.e a message bus locator. The CLI command now expects a mandatory + as first constructor argument, i.e a message bus locator. The CLI command now expects a mandatory `--bus` option value if there is more than one bus in the locator. * `MessageSubscriberInterface::getHandledMessages()` return value has changed. The value of an array item needs to be an associative array or the method name. @@ -343,6 +343,7 @@ Security * `SimpleAuthenticatorInterface`, `SimpleFormAuthenticatorInterface`, `SimplePreAuthenticatorInterface`, `SimpleAuthenticationProvider`, `SimpleAuthenticationHandler`, `SimpleFormAuthenticationListener` and `SimplePreAuthenticationListener` have been deprecated. Use Guard instead. + * **BC break note**: Upgrade to this version will log out all logged in users. See bug #33473. SecurityBundle -------------- diff --git a/UPGRADE-4.3.md b/UPGRADE-4.3.md index b85e09dadc320..5093de27cb2dc 100644 --- a/UPGRADE-4.3.md +++ b/UPGRADE-4.3.md @@ -54,7 +54,36 @@ Dotenv EventDispatcher --------------- - * The signature of the `EventDispatcherInterface::dispatch()` method should be updated to `dispatch($event, string $eventName = null)`, not doing so is deprecated + * The signature of the `EventDispatcherInterface::dispatch()` method has been updated, consider using the new signature `dispatch($event, string $eventName = null)` instead of the old signature `dispatch($eventName, $event)` that is deprecated + + You have to swap arguments when calling `dispatch()`: + + Before: + ```php + $this->eventDispatcher->dispatch(Events::My_EVENT, $event); + ``` + + After: + ```php + $this->eventDispatcher->dispatch($event, Events::My_EVENT); + ``` + + If your bundle or package needs to provide compatibility with the previous way of using the dispatcher, you can use `Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy::decorate()` to ease upgrades: + + Before: + ```php + public function __construct(EventDispatcherInterface $eventDispatcher) { + $this->eventDispatcher = $eventDispatcher; + } + ``` + + After: + ```php + public function __construct(EventDispatcherInterface $eventDispatcher) { + $this->eventDispatcher = LegacyEventDispatcherProxy::decorate($eventDispatcher); + } + ``` + * The `Event` class has been deprecated, use `Symfony\Contracts\EventDispatcher\Event` instead Filesystem @@ -77,7 +106,7 @@ Form FrameworkBundle --------------- - * Deprecated the `framework.templating` option, use Twig instead. + * Deprecated the `framework.templating` option, configure the Twig bundle instead. * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will be mandatory in 5.0. * Deprecated the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cach 8000 e\CacheInterface" / "cache.app" instead. @@ -180,11 +209,6 @@ Security * Not implementing the methods `__serialize` and `__unserialize` in classes implementing the `TokenInterface` is deprecated -SecurityBundle --------------- - - * Configuring encoders using `argon2i` or `bcrypt` as algorithm has been deprecated, use `auto` instead. - TwigBridge ---------- @@ -213,6 +237,28 @@ Workflow initial_marking: [draft] ``` + * `WorkflowInterface::apply()` will have a third argument in Symfony 5.0. + + Before: + ```php + class MyWorkflow implements WorkflowInterface + { + public function apply($subject, $transitionName) + { + } + } + ``` + + After: + ```php + class MyWorkflow implements WorkflowInterface + { + public function apply($subject, $transitionName, array $context = []) + { + } + } + ``` + * `MarkingStoreInterface::setMarking()` will have a third argument in Symfony 5.0. Before: diff --git a/UPGRADE-4.4.md b/UPGRADE-4.4.md index bb29a880159d9..c0160192746c3 100644 --- a/UPGRADE-4.4.md +++ b/UPGRADE-4.4.md @@ -5,51 +5,45 @@ Cache ----- * Added argument `$prefix` to `AdapterInterface::clear()` + * Marked the `CacheDataCollector` class as `@final`. + +Console +------- + + * Deprecated finding hidden commands using an abbreviation, use the full name instead + * Deprecated returning `null` from `Command::execute()`, return `0` instead + * Deprecated the `Application::renderException()` and `Application::doRenderException()` methods, + use `renderThrowable()` and `doRenderThrowable()` instead. Debug ----- - * Deprecated `FlattenException`, use the `FlattenException` of the `ErrorRenderer` component - * Deprecated the whole component in favor of `ErrorHandler` component + * Deprecated the component in favor of the `ErrorHandler` component + * Replace uses of `Symfony\Component\Debug\Debug` by `Symfony\Component\ErrorHandler\Debug` + +Config +------ + + * Deprecated overriding the `FilerLoader::import()` method without declaring the optional `$exclude` argument DependencyInjection ------------------- + * Made singly-implemented interfaces detection be scoped by file * Deprecated support for short factories and short configurators in Yaml Before: ```yaml services: - my_service: - factory: factory_service:method - ``` - - After: - ```yaml - services: - my_service: - factory: ['@factory_service', method] - ``` - * Deprecated `tagged` in favor of `tagged_iterator` - - Before: - ```yaml - services: - App\Handler: - tags: ['app.handler'] - - App\HandlerCollection: - arguments: [!tagged app.handler] + my_service: + factory: factory_service:method ``` After: ```yaml services: - App\Handler: - tags: ['app.handler'] - - App\HandlerCollection: - arguments: [!tagged_iterator app.handler] + my_service: + factory: ['@factory_service', method] ``` * Passing an instance of `Symfony\Component\DependencyInjection\Parameter` as class name to `Symfony\Component\DependencyInjection\Definition` is deprecated. @@ -64,6 +58,16 @@ DependencyInjection new Definition('%my_class%'); ``` +DoctrineBridge +-------------- + * Deprecated injecting `ClassMetadataFactory` in `DoctrineExtractor`, an instance of `EntityManagerInterface` should be + injected instead. + * Deprecated passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field. + * Deprecated not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field. + * Deprecated `RegistryInterface`, use `Doctrine\Persistence\ManagerRegistry`. + * Added a new `getMetadataDriverClass` method to replace class parameters in `AbstractDoctrineExtension`. This method + will be abstract in Symfony 5 and must be declared in extending classes. + Filesystem ---------- @@ -72,17 +76,24 @@ Filesystem Form ---- + * Using different values for the "model_timezone" and "view_timezone" options of the `TimeType` without configuring a + reference date is deprecated. * Using `int` or `float` as data for the `NumberType` when the `input` option is set to `string` is deprecated. + * Overriding the methods `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` without the `void` return-type is deprecated. FrameworkBundle --------------- - * Deprecated booting the kernel before running `WebTestCase::createClient()`. + * Deprecated calling `WebTestCase::createClient()` while a kernel has been booted, ensure the kernel is shut down before calling the method * Deprecated support for `templating` engine in `TemplateController`, use Twig instead * The `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` has been deprecated. * The `ControllerResolver` and `DelegatingLoader` classes have been marked as `final`. * The `controller_name_converter` and `resolve_controller_name_subscriber` services have been deprecated. + * Deprecated `routing.loader.service`, use `routing.loader.container` instead. + * Not tagging service route loaders with `routing.route_loader` has been deprecated. + * Overriding the methods `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` without the `void` return-type is deprecated. + * Marked the `RouterDataCollector` class as `@final`. HttpClient ---------- @@ -93,74 +104,293 @@ HttpFoundation -------------- * `ApacheRequest` is deprecated, use `Request` class instead. + * Passing a third argument to `HeaderBag::get()` is deprecated since Symfony 4.4, use method `all()` instead + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. + * `PdoSessionHandler` now precalculates the expiry timestamp in the lifetime column, + make sure to run `CREATE INDEX EXPIRY ON sessions (sess_lifetime)` to update your database + to speed up garbage collection of expired sessions. HttpKernel ---------- - * Implementing the `BundleInterface` without implementing the `getPublicDir()` method is deprecated. - This method will be added to the interface in 5.0. * The `DebugHandlersListener` class has been marked as `final` + * Added new Bundle directory convention consistent with standard skeletons: + + ``` + └── MyBundle/ + β”œβ”€β”€ config/ + β”œβ”€β”€ public/ + β”œβ”€β”€ src/ + β”‚ └── MyBundle.php + β”œβ”€β”€ templates/ + └── translations/ + ``` + + To make this work properly, it is necessary to change the root path of the bundle: + + ```php + class MyBundle extends Bundle + { + public function getPath(): string + { + return \dirname(__DIR__); + } + } + ``` + + As many bundles must be compatible with a range of Symfony versions, the current + directory convention is not deprecated yet, but it will be in the future. + + * Deprecated the second and third argument of `KernelInterface::locateResource` + * Deprecated the second and third argument of `FileLocator::__construct` + * Deprecated loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as + fallback directories. Resources like service definitions are usually loaded relative to the + current directory or with a glob pattern. The fallback directories have never been advocated + so you likely do not use those in any app based on the SF Standard or Flex edition. + * Getting the container from a non-booted kernel is deprecated + * Marked the `AjaxDataCollector`, `ConfigDataCollector`, `EventDataCollector`, + `ExceptionDataCollector`, `LoggerDataCollector`, `MemoryDataCollector`, + `RequestDataCollector` and `TimeDataCollector` classes as `@final`. + * Marked the `RouterDataCollector::collect()` method as `@final`. + * The `DataCollectorInterface::collect()` and `Profiler::collect()` methods third parameter signature + will be `\Throwable $exception = null` instead of `\Exception $exception = null` in Symfony 5.0. + * Deprecated methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead + * Deprecated class `ExceptionListener`, use `ErrorListener` instead Lock ---- * Deprecated `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and - `Symfony\Component\Lock\PersistStoreInterface`. + `Symfony\Component\Lock\PersistingStoreInterface`. * `Factory` is deprecated, use `LockFactory` instead + * Deprecated services `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract`, + use `StoreFactory::createStore` instead. + +Mailer +------ + + * [BC BREAK] Changed the DSN to use for disabling delivery (using the `NullTransport`) from `smtp://null` to `null://null` (host doesn't matter). + * [BC BREAK] Renamed class `SmtpEnvelope` to `Envelope` and `DelayedSmtpEnvelope` to `DelayedEnvelope`. + * [BC BREAK] Added a required `string $transport` argument to `MessageEvent::__construct`. Messenger --------- - * Deprecated passing a `ContainerInterface` instance as first argument of the `ConsumeMessagesCommand` constructor, - pass a `RoutableMessageBus` instance instead. + * [BC BREAK] Removed `SendersLocatorInterface::getSenderByAlias` added in 4.3. + * [BC BREAK] Removed `$retryStrategies` argument from `Worker::__construct`. + * [BC BREAK] Changed arguments of `ConsumeMessagesCommand::__construct`. + * [BC BREAK] Removed `$senderClassOrAlias` argument from `RedeliveryStamp::__construct`. + * [BC BREAK] Removed `UnknownSenderException`. + * [BC BREAK] Removed `WorkerInterface`. + * [BC BREAK] Removed `$onHandledCallback` of `Worker::run(array $options = [], callable $onHandledCallback = null)`. + * [BC BREAK] Removed `StopWhenMemoryUsageIsExceededWorker` in favor of `StopWorkerOnMemoryLimitListener`. + * [BC BREAK] Removed `StopWhenMessageCountIsExceededWorker` in favor of `StopWorkerOnMessageLimitListener`. + * [BC BREAK] Removed `StopWhenTimeLimitIsReachedWorker` in favor of `StopWorkerOnTimeLimitListener`. + * [BC BREAK] Removed `StopWhenRestartSignalIsReceived` in favor of `StopWorkerOnRestartSignalListener`. + * Marked the `MessengerDataCollector` class as `@final`. + +Mime +---- + + * Removed `NamedAddress`, use `Address` instead (which supports a name now) MonologBridge -------------- * The `RouteProcessor` has been marked final. +Process +------- + + * Deprecated the `Process::inheritEnvironmentVariables()` method: env variables are always inherited. + PropertyAccess -------------- * Deprecated passing `null` as 2nd argument of `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` instead. +Routing +------- + + * Deprecated `ServiceRouterLoader` in favor of `ContainerLoader`. + * Deprecated `ObjectRouteLoader` in favor of `ObjectLoader`. + Security -------- + * The `LdapUserProvider` class has been deprecated, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead. * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` should add a new `needsRehash()` method + * Deprecated returning a non-boolean value when implementing `Guard\AuthenticatorInterface::checkCredentials()`. Please explicitly return `false` to indicate invalid credentials. + * The `ListenerInterface` is deprecated, extend `AbstractListener` instead. + * Deprecated passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` (and indirectly the `is_granted()` Twig and ExpressionLanguage function) + + **Before** + ```php + if ($this->authorizationChecker->isGranted(['ROLE_USER', 'ROLE_ADMIN'])) { + // ... + } + ``` + + **After** + ```php + if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} + + // or: + if ($this->authorizationChecker->isGranted('ROLE_USER') + || $this->authorizationChecker->isGranted('ROLE_ADMIN') + ) {} + ``` + +SecurityBundle +-------------- + + * Marked the `SecurityDataCollector` class as `@final`. + +Serializer +---------- + + * Deprecated the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant. Use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead. Stopwatch --------- * Deprecated passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. +Translation +----------- + + * Deprecated support for using `null` as the locale in `Translator`. + * Deprecated accepting STDIN implicitly when using the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. + * Marked the `TranslationDataCollector` class as `@final`. + TwigBridge ---------- * Deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the `DebugCommand::__construct()` method, swap the variables position. + * Deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. + * Marked the `TwigDataCollector` class as `@final`. + +TwigBundle +---------- + + * Deprecated `twig.exception_controller` configuration option. + + If you were not using this option previously, set it to `null`: + + After: + ```yaml + twig: + exception_controller: null + ``` + + If you were using this option previously, set it to `null` and use `framework.error_controller` instead: + + Before: + ```yaml + twig: + exception_controller: 'App\Controller\MyExceptionController' + ``` + + After: + ```yaml + twig: + exception_controller: null + + framework: + error_controller: 'App\Controller\MyExceptionController' + ``` + + The new default exception controller will also change the error response content according to + https://tools.ietf.org/html/rfc7807 for `json`, `xml`, `atom` and `txt` formats: + + Before (HTTP status code `200`): + ```json + { + "error": { + "code": 404, + "message": "Sorry, the page you are looking for could not be found" + } + } + ``` + + After (HTTP status code `404`): + ```json + { + "title": "Not Found", + "status": 404, + "detail": "Sorry, the page you are looking for could not be found" + } + ``` + + * Deprecated the `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the HttpKernel component instead + * Deprecated all built-in error templates, use the error renderer mechanism of the `ErrorHandler` component + * Deprecated loading custom error templates in non-html formats. Custom HTML error pages based on Twig keep working as before: + + Before (`templates/bundles/TwigBundle/Exception/error.json.twig`): + ```twig + { + "type": "https://example.com/error", + "title": "{{ status_text }}", + "status": {{ status_code }} + } + ``` + + After (`App\Serializer\ProblemJsonNormalizer`): + ```php + class ProblemJsonNormalizer implements NormalizerInterface + { + public function normalize($exception, $format = null, array $context = []) + { + return [ + 'type' => 'https://example.com/error', + 'title' => $exception->getStatusText(), + 'status' => $exception->getStatusCode(), + ]; + } + + public function supportsNormalization($data, $format = null) + { + return 'json' === $format && $data instanceof FlattenException; + } + } + ``` Validator --------- + * [BC BREAK] Using null as `$classValidatorRegexp` value in `DoctrineLoader::__construct` or `PropertyInfoLoader::__construct` will not enable auto-mapping for all classes anymore, use `'{.*}'` instead. * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. * Deprecated using anything else than a `string` as the code of a `ConstraintViolation`, a `string` type-hint will be added to the constructor of the `ConstraintViolation` class and to the `ConstraintViolationBuilder::setCode()` method in 5.0. - * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. + * Deprecated passing an `ExpressionLanguage` instance as the second argument of `ExpressionValidator::__construct()`. Pass it as the first argument instead. * The `Length` constraint expects the `allowEmptyString` option to be defined when the `min` option is used. Set it to `true` to keep the current behavior and `false` to reject empty strings. In 5.0, it'll become optional and will default to `false`. + * Overriding the methods `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` without the `void` return-type is deprecated. + * deprecated `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all implementations in favor of PSR-6. + * deprecated `ValidatorBuilder::setMetadataCache`, use `ValidatorBuilder::setMappingCache` instead. + * The `Range` constraint has a new message option `notInRangeMessage` that is used when both `min` and `max` values are set. + In case you are using custom translations make sure to add one for this new message. + * Marked the `ValidatorDataCollector` class as `@final`. WebProfilerBundle ----------------- - * Deprecated the `ExceptionController::templateExists()` method + * Deprecated the `ExceptionController` class in favor of `ExceptionErrorController` * Deprecated the `TemplateManager::templateExists()` method WebServerBundle --------------- * The bundle is deprecated and will be removed in 5.0. + +Yaml +---- + + * Deprecated accepting STDIN implicitly when using the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. diff --git a/UPGRADE-5.0.md b/UPGRADE-5.0.md index 4795d2c5de00a..17dbdc4759b88 100644 --- a/UPGRADE-5.0.md +++ b/UPGRADE-5.0.md @@ -27,15 +27,20 @@ Config * Removed `FileLoaderLoadException`, use `LoaderLoadException` instead. * Using environment variables with `cannotBeEmpty()` if the value is validated with `validate()` will throw an exception. * Removed the `root()` method in `TreeBuilder`, pass the root node information to the constructor instead + * The `FilerLoader::import()` method has a new `$exclude` argument. Console ------- + * Removed support for finding hidden commands using an abbreviation, use the full name instead * Removed the `setCrossingChar()` method in favor of the `setDefaultCrossingChar()` method in `TableStyle`. * Removed the `setHorizontalBorderChar()` method in favor of the `setDefaultCrossingChars()` method in `TableStyle`. * Removed the `getHorizontalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. * Removed the `setVerticalBorderChar()` method in favor of the `setVerticalBorderChars()` method in `TableStyle`. * Removed the `getVerticalBorderChar()` method in favor of the `getBorderChars()` method in `TableStyle`. + * Removed support for returning `null` from `Command::execute()`, return `0` instead + * Renamed `Application::renderException()` and `Application::doRenderException()` + to `renderThrowable()` and `doRenderThrowable()` respectively. * The `ProcessHelper::run()` method takes the command as an array of arguments. Before: @@ -54,7 +59,8 @@ Console Debug ----- - * Removed the component + * Removed the component in favor of the `ErrorHandler` component + * Replace uses of `Symfony\Component\Debug\Debug` by `Symfony\Component\ErrorHandler\Debug` DependencyInjection ------------------- @@ -90,35 +96,16 @@ DependencyInjection my_service: factory: ['@factory_service', method] ``` - * Removed `tagged` in favor of `tagged_iterator` - - Before: - ```yaml - services: - App\Handler: - tags: ['app.handler'] - - App\HandlerCollection: - arguments: [!tagged app.handler] - ``` - - After: - ```yaml - services: - App\Handler: - tags: ['app.handler'] - - App\HandlerCollection: - arguments: [!tagged_iterator app.handler] - ``` DoctrineBridge -------------- - * Deprecated injecting `ClassMetadataFactory` in `DoctrineExtractor`, an instance of `EntityManagerInterface` should be + * Removed the possibility to inject `ClassMetadataFactory` in `DoctrineExtractor`, an instance of `EntityManagerInterface` should be injected instead * Passing an `IdReader` to the `DoctrineChoiceLoader` when the query cannot be optimized with single id field will throw an exception, pass `null` instead * Not passing an `IdReader` to the `DoctrineChoiceLoader` when the query can be optimized with single id field will not apply any optimization + * The `RegistryInterface` has been removed. + * Added a new `getMetadataDriverClass` method in `AbstractDoctrineExtension` to replace class parameters. DomCrawler ---------- @@ -152,6 +139,8 @@ Finder Form ---- + * Removed support for using different values for the "model_timezone" and "view_timezone" options of the `TimeType` + without configuring a reference date. * Removed support for using `int` or `float` as data for the `NumberType` when the `input` option is set to `string`. * Removed support for using the `format` option of `DateType` and `DateTimeType` when the `html5` option is enabled. * Using names for buttons that do not start with a letter, a digit, or an underscore leads to an exception. @@ -209,13 +198,13 @@ Form ``` * The `regions` option was removed from the `TimezoneType`. + * Added support for PHPUnit 8. A `void` return-type was added to the `FormIntegrationTestCase::setUp()`, `TypeTestCase::setUp()` and `TypeTestCase::tearDown()` methods. FrameworkBundle --------------- - * Dropped support for booting the kernel before running `WebTestCase::createClient()`. `createClient()` will throw an - exception if the kernel was already booted before. - * Removed the `framework.templating` option, use Twig instead. + * Calling `WebTestCase::createClient()` while a kernel has been booted now throws an exception, ensure the kernel is shut down before calling the method + * Removed the `framework.templating` option, configure the Twig bundle instead. * The project dir argument of the constructor of `AssetsInstallCommand` is required. * Removed support for `bundle:controller:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead where `serviceOrFqcn` is either the service ID when using controllers as services or the FQCN of the controller. @@ -250,6 +239,11 @@ FrameworkBundle * Support for the legacy directory structure in `translation:update` and `debug:translation` commands has been removed. * Removed the "Psr\SimpleCache\CacheInterface" / "cache.app.simple" service, use "Symfony\Contracts\Cache\CacheInterface" / "cache.app" instead. * Removed support for `templating` engine in `TemplateController`, use Twig instead + * Removed `ResolveControllerNameSubscriber`. + * Removed `routing.loader.service`. + * Added support for PHPUnit 8. A `void` return-type was added to the `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` method. + * Removed the `lock.store.flock`, `lock.store.semaphore`, `lock.store.memcached.abstract` and `lock.store.redis.abstract` services. + * Removed the `router.cache_class_prefix` parameter. HttpClient ---------- @@ -277,11 +271,15 @@ HttpFoundation * The `FileinfoMimeTypeGuesser` class has been removed, use `Symfony\Component\Mime\FileinfoMimeTypeGuesser` instead. * `ApacheRequest` has been removed, use the `Request` class instead. + * The third argument of the `HeaderBag::get()` method has been removed, use method `all()` instead. + * Getting the container from a non-booted kernel is not possible anymore. + * [BC BREAK] `PdoSessionHandler` with MySQL changed the type of the lifetime column, + make sure to run `ALTER TABLE sessions MODIFY sess_lifetime INTEGER UNSIGNED NOT NULL` to + update your database. HttpKernel ---------- - * The `getPublicDir()` method has been added to the `BundleInterface`. * Removed `Client`, use `HttpKernelBrowser` instead * The `Kernel::getRootDir()` and the `kernel.root_dir` parameter have been removed * The `KernelInterface::getName()` and the `kernel.name` parameter have been removed @@ -297,6 +295,39 @@ HttpKernel * Removed `PostResponseEvent`, use `TerminateEvent` instead * Removed `TranslatorListener` in favor of `LocaleAwareListener` * The `DebugHandlersListener` class has been made `final` + * Removed `SaveSessionListener` in favor of `AbstractSessionListener` + * Removed methods `ExceptionEvent::get/setException()`, use `get/setThrowable()` instead + * Removed class `ExceptionListener`, use `ErrorListener` instead + * Added new Bundle directory convention consistent with standard skeletons: + + ``` + └── MyBundle/ + β”œβ”€β”€ config/ + β”œβ”€β”€ public/ + β”œβ”€β”€ src/ + β”‚ └── MyBundle.php + β”œβ”€β”€ templates/ + └── translations/ + ``` + + To make this work properly, it is necessary to change the root path of the bundle: + + ```php + class MyBundle extends Bundle + { + public function getPath(): string + { + return \dirname(__DIR__); + } + } + ``` + + As many bundles must be compatible with a range of Symfony versions, the current + directory convention is not deprecated yet, but it will be in the future. + * Removed the second and third argument of `KernelInterface::locateResource` + * Removed the second and third argument of `FileLocator::__construct` + * Removed loading resources from `%kernel.root_dir%/Resources` and `%kernel.root_dir%` as + fallback directories. Intl ---- @@ -311,7 +342,7 @@ Lock ---- * Removed `Symfony\Component\Lock\StoreInterface` in favor of `Symfony\Component\Lock\BlockingStoreInterface` and - `Symfony\Component\Lock\PersistStoreInterface`. + `Symfony\Component\Lock\PersistingStoreInterface`. * Removed `Factory`, use `LockFactory` instead Messenger @@ -329,11 +360,12 @@ Monolog MonologBridge -------------- -* The `RouteProcessor` class is final. + * The `RouteProcessor` class is final. Process ------- + * Removed the `Process::inheritEnvironmentVariables()` method: env variables are always inherited. * Removed the `Process::setCommandline()` and the `PhpProcess::setPhpBinary()` methods. * Commands must be defined as arrays when creating a `Process` instance. @@ -361,14 +393,36 @@ Routing ------- * The `generator_base_class`, `generator_cache_class`, `matcher_base_class`, and `matcher_cache_class` router - options have been removed. + options have been removed. If you are using multiple Router instances and need separate caches for them, set a unique `cache_dir` per Router instance instead. * `Serializable` implementing methods for `Route` and `CompiledRoute` are final. Instead of overwriting them, use `__serialize` and `__unserialize` as extension points which are forward compatible with the new serialization methods in PHP 7.4. + * Removed `ServiceRouterLoader` and `ObjectRouteLoader`. + * Service route loaders must be tagged with `routing.route_loader`. + * The `RoutingConfigurator::import()` method has a new optional `$exclude` argument. Security -------- + * Dropped support for passing more than one attribute to `AccessDecisionManager::decide()` and `AuthorizationChecker::isGranted()` (and indirectly the `is_granted()` Twig and ExpressionLanguage function): + + **Before** + ```php + if ($this->authorizationChecker->isGranted(['ROLE_USER', 'ROLE_ADMIN'])) { + // ... + } + ``` + + **After** + ```php + if ($this->authorizationChecker->isGranted(new Expression("is_granted('ROLE_USER') or is_granted('ROLE_ADMIN')"))) {} + + // or: + if ($this->authorizationChecker->isGranted('ROLE_USER') + || $this->authorizationChecker->isGranted('ROLE_ADMIN') + ) {} + ``` + * The `LdapUserProvider` class has been removed, use `Symfony\Component\Ldap\Security\LdapUserProvider` instead. * Implementations of `PasswordEncoderInterface` and `UserPasswordEncoderInterface` must have a new `needsRehash()` method * The `Role` and `SwitchUserRole` classes have been removed. * The `getReachableRoles()` method of the `RoleHierarchy` class has been removed. It has been replaced by the new @@ -385,7 +439,7 @@ Security * `SimpleAuthenticatorInterface`, `SimpleFormAuthenticatorInterface`, `SimplePreAuthenticatorInterface`, `SimpleAuthenticationProvider`, `SimpleAuthenticationHandler`, `SimpleFormAuthenticationListener` and `SimplePreAuthenticationListener` have been removed. Use Guard instead. - * The `ListenerInterface` has been removed, turn your listeners into callables instead. + * The `ListenerInterface` has been removed, extend `AbstractListener` instead. * The `Firewall::handleRequest()` method has been removed, use `Firewall::callListeners()` instead. * `\Serializable` interface has been removed from `AbstractToken` and `AuthenticationException`, thus `serialize()` and `unserialize()` aren't available. @@ -423,6 +477,8 @@ Security * The `BCryptPasswordEncoder` class has been removed, use `NativePasswordEncoder` instead. * Classes implementing the `TokenInterface` must implement the two new methods `__serialize` and `__unserialize` + * Implementations of `Guard\AuthenticatorInterface::checkCredentials()` must return a boolean value now. Please explicitly return `false` to indicate invalid credentials. + * Removed the `has_role()` function from security expressions, use `is_granted()` instead. SecurityBundle -------------- @@ -443,22 +499,50 @@ SecurityBundle changed to underscores. Before: `my-cookie` deleted the `my_cookie` cookie (with an underscore). After: `my-cookie` deletes the `my-cookie` cookie (with a dash). - * Configuring encoders using `argon2i` or `bcrypt` as algorithm is not supported anymore, use `auto` instead. + * Removed the `security.user.provider.in_memory.user` service. Serializer ---------- + * The default value of the `CsvEncoder` "as_collection" option was changed to `true`. + * Individual encoders & normalizers options as constructor arguments were removed. + Use the default context instead. + * The following method and properties: + - `AbstractNormalizer::$circularReferenceLimit` + - `AbstractNormalizer::$circularReferenceHandler` + - `AbstractNormalizer::$callbacks` + - `AbstractNormalizer::$ignoredAttributes` + - `AbstractNormalizer::$camelizedAttributes` + - `AbstractNormalizer::setCircularReferenceLimit()` + - `AbstractNormalizer::setCircularReferenceHandler()` + - `AbstractNormalizer::setCallbacks()` + - `AbstractNormalizer::setIgnoredAttributes()` + - `AbstractObjectNormalizer::$maxDepthHandler` + - `AbstractObjectNormalizer::setMaxDepthHandler()` + - `XmlEncoder::setRootNodeName()` + - `XmlEncoder::getRootNodeName()` + + were removed, use the default context instead. * The `AbstractNormalizer::handleCircularReference()` method has two new `$format` and `$context` arguments. + * Removed support for instantiating a `DataUriNormalizer` with a default MIME type guesser when the `symfony/mime` component isn't installed. + * Removed the `XmlEncoder::TYPE_CASE_ATTRIBUTES` constant. Use `XmlEncoder::TYPE_CAST_ATTRIBUTES` instead. + +Stopwatch +--------- + + * Removed support for passing `null` as 1st (`$id`) argument of `Section::get()` method, pass a valid child section identifier instead. Translation ----------- + * Support for using `null` as the locale in `Translator` has been removed. * The `FileDumper::setBackup()` method has been removed. * The `TranslationWriter::disableBackup()` method has been removed. * The `TranslatorInterface` has been removed in favor of `Symfony\Contracts\Translation\TranslatorInterface` * The `MessageSelector`, `Interval` and `PluralizationRules` classes have been removed, use `IdentityTranslator` instead * The `Translator::getFallbackLocales()` and `TranslationDataCollector::getFallbackLocales()` method are now internal * The `Translator::transChoice()` method has been removed in favor of using `Translator::tran 8000 s()` with "%count%" as the parameter driving plurals + * Removed support for implicit STDIN usage in the `lint:xliff` command, use `lint:xliff -` (append a dash) instead to make it explicit. TwigBundle ---------- @@ -466,6 +550,8 @@ TwigBundle * The default value (`false`) of the `twig.strict_variables` configuration option has been changed to `%kernel.debug%`. * The `transchoice` tag and filter have been removed, use the `trans` ones instead with a `%count%` parameter. * Removed support for legacy templates directories `src/Resources/views/` and `src/Resources//views/`, use `templates/` and `templates/bundles//` instead. + * The `twig.exception_controller` configuration option has been removed, use `framework.error_controller` instead. + * Removed `ExceptionController`, `PreviewErrorController` classes and all built-in error templates TwigBridge ---------- @@ -474,6 +560,7 @@ TwigBridge * removed the `$requestStack` and `$requestContext` arguments of the `HttpFoundationExtension`, pass a `Symfony\Component\HttpFoundation\UrlHelper` instance as the only argument instead + * Removed support for implicit STDIN usage in the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. Validator -------- @@ -491,6 +578,10 @@ Validator * The `symfony/intl` component is now required for using the `Bic`, `Country`, `Currency`, `Language` and `Locale` constraints * The `egulias/email-validator` component is now required for using the `Email` constraint in strict mode * The `symfony/expression-language` component is now required for using the `Expression` constraint + * Changed the default value of `Length::$allowEmptyString` to `false` and made it optional + * Added support for PHPUnit 8. A `void` return-type was added to the `ConstraintValidatorTestCase::setUp()` and `ConstraintValidatorTestCase::tearDown()` methods. + * The `Symfony\Component\Validator\Mapping\Cache\CacheInterface` and all its implementations have been removed. + * The `ValidatorBuilder::setMetadataCache` has been removed, use `ValidatorBuilder::setMappingCache` instead. WebProfilerBundle ----------------- @@ -505,8 +596,9 @@ Workflow * `add` method has been removed use `addWorkflow` method in `Workflow\Registry` instead. * `SupportStrategyInterface` has been removed, use `WorkflowSupportStrategyInterface` instead. * `ClassInstanceSupportStrategy` has been removed, use `InstanceOfSupportStrategy` instead. + * `WorkflowInterface::apply()` has a third argument: `array $context = []`. * `MarkingStoreInterface::setMarking()` has a third argument: `array $context = []`. - * Removed support of `initial_place`. Use `initial_places` instead. + * Removed support of `initial_place`. Use `initial_marking` instead. * `MultipleStateMarkingStore` has been removed. Use `MethodMarkingStore` instead. * `DefinitionBuilder::setInitialPlace()` has been removed, use `DefinitionBuilder::setInitialPlaces()` instead. @@ -575,8 +667,14 @@ Yaml * The parser is now stricter and will throw a `ParseException` when a mapping is found inside a multi-line string. + * Removed support for implicit STDIN usage in the `lint:yaml` command, use `lint:yaml -` (append a dash) instead to make it explicit. + +WebProfilerBundle +----------------- + + * Removed the `ExceptionController` class, use `ExceptionErrorController` instead. WebServerBundle --------------- - * The bundle has been removed. + * The bundle has been deprecated and can be installed separately. You may also use the Symfony Local Web Server instead. diff --git a/composer.json b/composer.json index 64ef99d8e5023..25674294d0797 100644 --- a/composer.json +++ b/composer.json @@ -15,24 +15,42 @@ "homepage": "https://symfony.com/contributors" } ], + "provide": { + "php-http/async-client-implementation": "*", + "php-http/client-implementation": "*", + "psr/cache-implementation": "1.0|2.0", + "psr/container-implementation": "1.0", + "psr/event-dispatcher-implementation": "1.0", + "psr/http-client-implementation": "1.0", + "psr/link-implementation": "1.0", + "psr/log-implementation": "1.0|2.0", + "psr/simple-cache-implementation": "1.0|2.0", + "symfony/cache-implementation": "1.0|2.0", + "symfony/event-dispatcher-implementation": "1.1", + "symfony/http-client-implementation": "1.1|2.0", + "symfony/service-implementation": "1.0|2.0", + "symfony/translation-implementation": "1.0|2.0" + }, "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "ext-xml": "*", + "friendsofphp/proxy-manager-lts": "^1.0.2", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "~1.0", - "fig/link-util": "^1.0", - "twig/twig": "^1.41|^2.10", - "psr/cache": "~1.0", + "doctrine/persistence": "^1.3|^2|^3", + "twig/twig": "^1.43|^2.13|^3.0.4", + "psr/cache": "^1.0|^2.0", "psr/container": "^1.0", "psr/link": "^1.0", - "psr/log": "~1.0", - "symfony/contracts": "^1.1.3", + "psr/log": "^1|^2", + "symfony/contracts": "^1.1.8", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-mbstring": "~1.0", "symfony/polyfill-php72": "~1.5", - "symfony/polyfill-php73": "^1.11" + "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.16", + "symfony/polyfill-php81": "^1.22" }, "replace": { "symfony/asset": "self.version", @@ -48,7 +66,7 @@ "symfony/doctrine-bridge": "self.version", "symfony/dom-crawler": "self.version", "symfony/dotenv": "self.version", - "symfony/error-renderer": "self.version", + "symfony/error-handler": "self.version", "symfony/event-dispatcher": "self.version", "symfony/expression-language": "self.version", "symfony/filesystem": "self.version", @@ -100,34 +118,45 @@ }, "require-dev": { "cache/integration-tests": "dev-master", - "doctrine/annotations": "~1.0", - "doctrine/cache": "~1.6", + "composer/package-versions-deprecated": "^1.8", + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "^1.6|^2.0", "doctrine/collections": "~1.0", - "doctrine/data-fixtures": "1.0.*", - "doctrine/dbal": "~2.4", - "doctrine/orm": "~2.4,>=2.4.5", - "doctrine/reflection": "~1.0", - "doctrine/doctrine-bundle": "~1.4", + "doctrine/data-fixtures": "^1.1", + "doctrine/dbal": "^2.7|^3.0", + "doctrine/orm": "^2.6.3", + "guzzlehttp/promises": "^1.4", "masterminds/html5": "^2.6", - "monolog/monolog": "~1.11", + "monolog/monolog": "^1.25.1", "nyholm/psr7": "^1.0", - "ocramius/proxy-manager": "^2.1", + "paragonie/sodium_compat": "^1.8", "php-http/httplug": "^1.0|^2.0", "predis/predis": "~1.1", "psr/http-client": "^1.0", - "psr/simple-cache": "^1.0", - "egulias/email-validator": "~1.2,>=1.2.8|~2.0", - "symfony/phpunit-bridge": "~3.4|~4.0|~5.0", + "psr/simple-cache": "^1.0|^2.0", + "egulias/email-validator": "^2.1.10|^3.1", + "symfony/phpunit-bridge": "^5.2", "symfony/security-acl": "~2.8|~3.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0" + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" }, "conflict": { + "doctrine/dbal": "<2.7", + "egulias/email-validator": "~3.0.0", "masterminds/html5": "<2.6", - "phpdocumentor/reflection-docblock": "<3.0||>=3.2.0,<3.2.2", - "phpdocumentor/type-resolver": "<0.3.0", + "monolog/monolog": ">=2", + "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0|1.3.*", "ocramius/proxy-manager": "<2.1", "phpunit/phpunit": "<5.4.3" }, + "config": { + "allow-plugins": { + "symfony/runtime": true + } + }, "autoload": { "psr-4": { "Symfony\\Bridge\\Doctrine\\": "src/Symfony/Bridge/Doctrine/", @@ -150,13 +179,13 @@ "repositories": [ { "type": "path", - "url": "src/Symfony/Contracts" + "url": "src/Symfony/Contracts", + "options": { + "versions": { + "symfony/contracts": "1.1.x-dev" + } + } } ], - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/link b/link index 2b76a466c3248..60cd84dc4b569 100755 --- a/link +++ b/link @@ -2,12 +2,12 @@ -* -* For the full copyright and license information, please view the LICENSE -* file that was distributed with this source code. + * This file is part of the Symfony package. + * + * (c) Fabien Potencier + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. */ require __DIR__.'/src/Symfony/Component/Filesystem/Exception/ExceptionInterface.php'; @@ -18,16 +18,22 @@ require __DIR__.'/src/Symfony/Component/Filesystem/Filesystem.php'; use Symfony\Component\Filesystem\Filesystem; /** - * Links dependencies to components to a local clone of the main symfony/symfony GitHub repository. + * Links dependencies of a project to a local clone of the main symfony/symfony GitHub repository. * * @author KΓ©vin Dunglas */ +$copy = false !== $k = array_search('--copy', $argv, true); +$copy && array_splice($argv, $k, 1); +$rollback = false !== $k = array_search('--rollback', $argv, true); +$rollback && array_splice($argv, $k, 1); $pathToProject = $argv[1] ?? getcwd(); if (!is_dir("$pathToProject/vendor/symfony")) { - echo 'Link dependencies to components to a local clone of the main symfony/symfony GitHub repository.'.PHP_EOL.PHP_EOL; - echo "Usage: $argv[0] /path/to/the/project".PHP_EOL.PHP_EOL; + echo 'Links dependencies of a project to a local clone of the main symfony/symfony GitHub repository.'.PHP_EOL.PHP_EOL; + echo "Usage: $argv[0] /path/to/the/project".PHP_EOL; + echo ' Use `--copy` to copy dependencies instead of symlink'.PHP_EOL.PHP_EOL; + echo ' Use `--rollback` to rollback'.PHP_EOL.PHP_EOL; echo "The directory \"$pathToProject\" does not exist or the dependencies are not installed, did you forget to run \"composer install\" in your project?".PHP_EOL; exit(1); } @@ -35,7 +41,7 @@ if (!is_dir("$pathToProject/vendor/symfony")) { $sfPackages = array('symfony/symfony' => __DIR__); $filesystem = new Filesystem(); -$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security'); +$braces = array('Bundle', 'Bridge', 'Component', 'Component/Security', 'Component/Mailer/Bridge', 'Component/Messenger/Bridge', 'Component/Notifier/Bridge', 'Contracts'); $directories = array_merge(...array_values(array_map(function ($part) { return glob(__DIR__.'/src/Symfony/'.$part.'/*', GLOB_ONLYDIR | GLOB_NOSORT); }, $braces))); @@ -50,22 +56,39 @@ foreach ($directories as $dir) { foreach (glob("$pathToProject/vendor/symfony/*", GLOB_ONLYDIR | GLOB_NOSORT) as $dir) { $package = 'symfony/'.basename($dir); - if (is_link($dir)) { - echo "\"$package\" is already a symlink, skipping.".PHP_EOL; + + if (!isset($sfPackages[$package])) { continue; } - if (!isset($sfPackages[$package])) { + if ($rollback) { + $filesystem->remove($dir); + echo "\"$package\" has been rollback from \"$sfPackages[$package]\".".PHP_EOL; + continue; + } + + if (!$copy && is_link($dir)) { + echo "\"$package\" is already a symlink, skipping.".PHP_EOL; continue; } - $sfDir = '\\' === DIRECTORY_SEPARATOR ? $sfPackages[$package] : $filesystem->makePathRelative($sfPackages[$package], dirname(realpath($dir))); + $sfDir = ('\\' === DIRECTORY_SEPARATOR || $copy) ? $sfPackages[$package] : $filesystem->makePathRelative($sfPackages[$package], dirname(realpath($dir))); $filesystem->remove($dir); - $filesystem->symlink($sfDir, $dir); - echo "\"$package\" has been linked to \"$sfPackages[$package]\".".PHP_EOL; + + if ($copy) { + $filesystem->mirror($sfDir, $dir); + echo "\"$package\" has been copied from \"$sfPackages[$package]\".".PHP_EOL; + } else { + $filesystem->symlink($sfDir, $dir); + echo "\"$package\" has been linked to \"$sfPackages[$package]\".".PHP_EOL; + } } -foreach (glob("$pathToProject/var/cache/*") as $cacheDir) { +foreach (glob("$pathToProject/var/cache/*", GLOB_NOSORT) as $cacheDir) { $filesystem->remove($cacheDir); } + +if ($rollback) { + echo PHP_EOL."Rollback done, do not forget to run \"composer install\" in your project \"$pathToProject\".".PHP_EOL; +} diff --git a/phpunit b/phpunit index 6d5bdc279bcd7..e26fecd73cc9d 100755 --- a/phpunit +++ b/phpunit @@ -1,14 +1,28 @@ #!/usr/bin/env php = 70000 && !getenv('SYMFONY_PHPUNIT_VERSION')) { - putenv('SYMFONY_PHPUNIT_VERSION=6.5'); +if (!getenv('SYMFONY_PHPUNIT_VERSION')) { + if (\PHP_VERSION_ID < 70200) { + putenv('SYMFONY_PHPUNIT_VERSION=7.5'); + } elseif (\PHP_VERSION_ID < 70300) { + putenv('SYMFONY_PHPUNIT_VERSION=8.5.26'); + } else { + putenv('SYMFONY_PHPUNIT_VERSION=9.5'); + } +} +if (!getenv('SYMFONY_PATCH_TYPE_DECLARATIONS') && \PHP_VERSION_ID >= 70300) { + putenv('SYMFONY_PATCH_TYPE_DECLARATIONS=deprecations=1'); +} +if (getcwd() === realpath(__DIR__.'/src/Symfony/Bridge/PhpUnit')) { + putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); } putenv('SYMFONY_PHPUNIT_DIR='.__DIR__.'/.phpunit'); require __DIR__.'/vendor/symfony/phpunit-bridge/bin/simple-phpunit'; diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7313d16d25c70..cd85992d44d55 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -15,9 +15,11 @@ - + + + @@ -28,6 +30,7 @@ ./src/Symfony/Bridge/*/Tests/ ./src/Symfony/Component/*/Tests/ ./src/Symfony/Component/*/*/Tests/ + ./src/Symfony/Component/*/*/*/Tests/ ./src/Symfony/Contract/*/Tests/ ./src/Symfony/Bundle/*/Tests/ diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 0000000000000..3fb94145699cf --- /dev/null +++ b/psalm.xml @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bridge/Doctrine/.gitattributes b/src/Symfony/Bridge/Doctrine/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bridge/Doctrine/CHANGELOG.md b/src/Symfony/Bridge/Doctrine/CHANGELOG.md index 7123b527ebc3d..8fe8d410797fa 100644 --- a/src/Symfony/Bridge/Doctrine/CHANGELOG.md +++ b/src/Symfony/Bridge/Doctrine/CHANGELOG.md @@ -4,8 +4,11 @@ CHANGELOG 4.4.0 ----- - * added `DoctrineClearEntityManagerMiddleware` - + * [BC BREAK] using null as `$classValidatorRegexp` value in `DoctrineLoader::__construct` will not enable auto-mapping for all classes anymore, use `'{.*}'` instead. + * added `DoctrineClearEntityManagerWorkerSubscriber` + * deprecated `RegistryInterface`, use `Doctrine\Persistence\ManagerRegistry` + * added support for invokable event listeners + * added `getMetadataDriverClass` method to deprecate class parameters in service configuration files 4.3.0 ----- diff --git a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php index 9bf22357df895..08f9fef880e51 100644 --- a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php +++ b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\Doctrine\CacheWarmer; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; /** @@ -49,7 +49,7 @@ public function warmUp($cacheDir) foreach ($this->registry->getManagers() as $em) { // we need the directory no matter the proxy cache generation strategy if (!is_dir($proxyCacheDir = $em->getConfiguration()->getProxyDir())) { - if (false === @mkdir($proxyCacheDir, 0777, true)) { + if (false === @mkdir($proxyCacheDir, 0777, true) && !is_dir($proxyCacheDir)) { throw new \RuntimeException(sprintf('Unable to create the Doctrine Proxy directory "%s".', $proxyCacheDir)); } } elseif (!is_writable($proxyCacheDir)) { diff --git a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php index 4496d3ac9a3d9..bb0f62a95c76f 100644 --- a/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php +++ b/src/Symfony/Bridge/Doctrine/ContainerAwareEventManager.php @@ -16,7 +16,7 @@ use Psr\Container\ContainerInterface; /** - * Allows lazy loading of listener services. + * Allows lazy loading of listener and subscriber services. * * @author Johannes M. Schmitt */ @@ -28,45 +28,67 @@ class ContainerAwareEventManager extends EventManager * => */ private $listeners = []; + private $subscribers; private $initialized = []; + private $initializedSubscribers = false; + private $methods = []; private $container; - public function __construct(ContainerInterface $container) + public function __construct(ContainerInterface $container, array $subscriberIds = []) { $this->container = $container; + $this->subscribers = $subscriberIds; } /** * {@inheritdoc} + * + * @return void */ public function dispatchEvent($eventName, EventArgs $eventArgs = null) { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } if (!isset($this->listeners[$eventName])) { return; } - $eventArgs = null === $eventArgs ? EventArgs::getEmptyInstance() : $eventArgs; + $eventArgs = $eventArgs ?? EventArgs::getEmptyInstance(); if (!isset($this->initialized[$eventName])) { $this->initializeListeners($eventName); } foreach ($this->listeners[$eventName] as $hash => $listener) { - $listener->$eventName($eventArgs); + $listener->{$this->methods[$eventName][$hash]}($eventArgs); } } /** * {@inheritdoc} + * + * @return object[][] */ public function getListeners($event = null) { - if (null !== $event) { - if (!isset($this->initialized[$event])) { - $this->initializeListeners($event); - } + if (null === $event) { + return $this->getAllListeners(); + } + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + if (!isset($this->initialized[$event])) { + $this->initializeListeners($event); + } + + return $this->listeners[$event]; + } - return $this->listeners[$event]; + public function getAllListeners(): array + { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); } foreach ($this->listeners as $event => $listeners) { @@ -80,23 +102,26 @@ public function getListeners($event = null) /** * {@inheritdoc} + * + * @return bool */ public function hasListeners($event) { + if (!$this->initializedSubscribers) { + $this->initializeSubscribers(); + } + return isset($this->listeners[$event]) && $this->listeners[$event]; } /** * {@inheritdoc} + * + * @return void */ public function addEventListener($events, $listener) { - if (\is_string($listener)) { - $hash = '_service_'.$listener; - } else { - // Picks the hash code related to that listener - $hash = spl_object_hash($listener); - } + $hash = $this->getHash($listener); foreach ((array) $events as $event) { // Overrides listener if a previous one was associated already @@ -105,40 +130,88 @@ public function addEventListener($events, $listener) if (\is_string($listener)) { unset($this->initialized[$event]); + } else { + $this->methods[$event][$hash] = $this->getMethod($listener, $event); } } } /** * {@inheritdoc} + * + * @return void */ public function removeEventListener($events, $listener) { - if (\is_string($listener)) { - $hash = '_service_'.$listener; - } else { - // Picks the hash code related to that listener - $hash = spl_object_hash($listener); - } + $hash = $this->getHash($listener); foreach ((array) $events as $event) { - // Check if actually have this listener associated + // Check if we actually have this listener associated if (isset($this->listeners[$event][$hash])) { unset($this->listeners[$event][$hash]); } + + if (isset($this->methods[$event][$hash])) { + unset($this->methods[$event][$hash]); + } } } - /** - * @param string $eventName - */ - private function initializeListeners($eventName) + private function initializeListeners(string $eventName) { + $this->initialized[$eventName] = true; foreach ($this->listeners[$eventName] as $hash => $listener) { if (\is_string($listener)) { - $this->listeners[$eventName][$hash] = $this->container->get($listener); + $this->listeners[$eventName][$hash] = $listener = $this->container->get($listener); + + $this->methods[$eventName][$hash] = $this->getMethod($listener, $eventName); } } - $this->initialized[$eventName] = true; + } + + private function initializeSubscribers() + { + $this->initializedSubscribers = true; + + $eventListeners = $this->listeners; + // reset eventListener to respect priority: EventSubscribers have a higher priority + $this->listeners = []; + foreach ($this->subscribers as $id => $subscriber) { + if (\is_string($subscriber)) { + parent::addEventSubscriber($this->subscribers[$id] = $this->container->get($subscriber)); + } + } + foreach ($eventListeners as $event => $listeners) { + if (!isset($this->listeners[$event])) { + $this->listeners[$event] = []; + } + unset($this->initialized[$event]); + $this->listeners[$event] += $listeners; + } + $this->subscribers = []; + } + + /** + * @param string|object $listener + */ + private function getHash($listener): string + { + if (\is_string($listener)) { + return '_service_'.$listener; + } + + return spl_object_hash($listener); + } + + /** + * @param object $listener + */ + private function getMethod($listener, string $event): string + { + if (!method_exists($listener, $event) && method_exists($listener, '__invoke')) { + return '__invoke'; + } + + return $event; } } diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php index ee4c644f0ea8a..24024cefd14c8 100644 --- a/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php +++ b/src/Symfony/Bridge/Doctrine/DataCollector/DoctrineDataCollector.php @@ -11,13 +11,15 @@ namespace Symfony\Bridge\Doctrine\DataCollector; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Logging\DebugStack; use Doctrine\DBAL\Types\ConversionException; use Doctrine\DBAL\Types\Type; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; +use Symfony\Component\VarDumper\Caster\Caster; +use Symfony\Component\VarDumper\Cloner\Stub; /** * DoctrineDataCollector. @@ -45,8 +47,7 @@ public function __construct(ManagerRegistry $registry) /** * Adds the stack logger for a connection. * - * @param string $name - * @param DebugStack $logger + * @param string $name */ public function addLogger($name, DebugStack $logger) { @@ -55,8 +56,10 @@ public function addLogger($name, DebugStack $logger) /** * {@inheritdoc} + * + * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response, \Exception $exception = null) + public function collect(Request $request, Response $response/* , \Throwable $exception = null */) { $queries = []; foreach ($this->loggers as $name => $logger) { @@ -120,7 +123,39 @@ public function getName() return 'db'; } - private function sanitizeQueries($connectionName, $queries) + /** + * {@inheritdoc} + */ + protected function getCasters() + { + return parent::getCasters() + [ + ObjectParameter::class => static function (ObjectParameter $o, array $a, Stub $s): array { + $s->class = $o->getClass(); + $s->value = $o->getObject(); + + $r = new \ReflectionClass($o->getClass()); + if ($f = $r->getFileName()) { + $s->attr['file'] = $f; + $s->attr['line'] = $r->getStartLine(); + } else { + unset($s->attr['file']); + unset($s->attr['line']); + } + + if ($error = $o->getError()) { + return [Caster::PREFIX_VIRTUAL.'⚠' => $error->getMessage()]; + } + + if ($o->isStringable()) { + return [Caster::PREFIX_VIRTUAL.'__toString()' => (string) $o->getObject()]; + } + + return [Caster::PREFIX_VIRTUAL.'⚠' => sprintf('Object of class "%s" could not be converted to string.', $o->getClass())]; + }, + ]; + } + + private function sanitizeQueries(string $connectionName, array $queries): array { foreach ($queries as $i => $query) { $queries[$i] = $this->sanitizeQuery($connectionName, $query); @@ -129,9 +164,10 @@ private function sanitizeQueries($connectionName, $queries) return $queries; } - private function sanitizeQuery($connectionName, $query) + private function sanitizeQuery(string $connectionName, array $query): array { $query['explainable'] = true; + $query['runnable'] = true; if (null === $query['params']) { $query['params'] = []; } @@ -142,6 +178,7 @@ private function sanitizeQuery($connectionName, $query) $query['types'] = []; } foreach ($query['params'] as $j => $param) { + $e = null; if (isset($query['types'][$j])) { // Transform the param according to the type $type = $query['types'][$j]; @@ -153,20 +190,23 @@ private function sanitizeQuery($connectionName, $query) try { $param = $type->convertToDatabaseValue($param, $this->registry->getConnection($connectionName)->getDatabasePlatform()); } catch (\TypeError $e) { - // Error thrown while processing params, query is not explainable. - $query['explainable'] = false; } catch (ConversionException $e) { - $query['explainable'] = false; } } } - list($query['params'][$j], $explainable) = $this->sanitizeParam($param); + [$query['params'][$j], $explainable, $runnable] = $this->sanitizeParam($param, $e); if (!$explainable) { $query['explainable'] = false; } + + if (!$runnable) { + $query['runnable'] = false; + } } + $query['params'] = $this->cloneVar($query['params']); + return $query; } @@ -177,32 +217,33 @@ private function sanitizeQuery($connectionName, $query) * indicating if the original value was kept (allowing to use the sanitized * value to explain the query). */ - private function sanitizeParam($var): array + private function sanitizeParam($var, ?\Throwable $error): array { if (\is_object($var)) { - $className = \get_class($var); + return [$o = new ObjectParameter($var, $error), false, 6D40 $o->isStringable() && !$error]; + } - return method_exists($var, '__toString') ? - [sprintf('/* Object(%s): */"%s"', $className, $var->__toString()), false] : - [sprintf('/* Object(%s) */', $className), false]; + if ($error) { + return ['⚠ '.$error->getMessage(), false, false]; } if (\is_array($var)) { $a = []; - $original = true; + $explainable = $runnable = true; foreach ($var as $k => $v) { - list($value, $orig) = $this->sanitizeParam($v); - $original = $original && $orig; + [$value, $e, $r] = $this->sanitizeParam($v, null); + $explainable = $explainable && $e; + $runnable = $runnable && $r; $a[$k] = $value; } - return [$a, $original]; + return [$a, $explainable, $runnable]; } if (\is_resource($var)) { - return [sprintf('/* Resource(%s) */', get_resource_type($var)), false]; + return [sprintf('/* Resource(%s) */', get_resource_type($var)), false, false]; } - return [$var, true]; + return [$var, true, true]; } } diff --git a/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.php new file mode 100644 index 0000000000000..26bdb7ff267d0 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/DataCollector/ObjectParameter.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\Bridge\Doctrine\DataCollector; + +final class ObjectParameter +{ + private $object; + private $error; + private $stringable; + private $class; + + /** + * @param object $object + */ + public function __construct($object, ?\Throwable $error) + { + $this->object = $object; + $this->error = $error; + $this->stringable = \is_callable([$object, '__toString']); + $this->class = \get_class($object); + } + + /** + * @return object + */ + public function getObject() + { + return $this->object; + } + + public function getError(): ?\Throwable + { + return $this->error; + } + + public function isStringable(): bool + { + return $this->stringable; + } + + public function getClass(): string + { + return $this->class; + } +} diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php index a36b55eb16c0c..2d9302ac141ea 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/AbstractDoctrineExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Doctrine\DependencyInjection; +use Symfony\Component\Config\Resource\GlobResource; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; @@ -35,8 +36,7 @@ abstract class AbstractDoctrineExtension extends Extension protected $drivers = []; /** - * @param array $objectManager A configured object manager - * @param ContainerBuilder $container A ContainerBuilder instance + * @param array $objectManager A configured object manager * * @throws \InvalidArgumentException */ @@ -89,6 +89,25 @@ protected function loadMappingInformation(array $objectManager, ContainerBuilder if (!$mappingConfig) { continue; } + } elseif (!$mappingConfig['type'] && \PHP_VERSION_ID < 80000) { + $mappingConfig['type'] = 'annotation'; + } elseif (!$mappingConfig['type']) { + $mappingConfig['type'] = 'attribute'; + + $glob = new GlobResource($mappingConfig['dir'], '*', true); + $container->addResource($glob); + + foreach ($glob as $file) { + $content = file_get_contents($file); + + if (preg_match('/^#\[.*Entity\b/m', $content)) { + break; + } + if (preg_match('/^ \* @.*Entity\b/m', $content)) { + $mappingConfig['type'] = 'annotation'; + break; + } + } } $this->assertValidMappingConfiguration($mappingConfig, $objectManager['name']); @@ -117,7 +136,6 @@ protected function setMappingDriverAlias($mappingConfig, $mappingName) /** * Register the mapping driver configuration for later use with the object managers metadata driver chain. * - * @param array $mappingConfig * @param string $mappingName * * @throws \InvalidArgumentException @@ -153,7 +171,7 @@ protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \Re } if (!$bundleConfig['dir']) { - if (\in_array($bundleConfig['type'], ['annotation', 'staticphp'])) { + if (\in_array($bundleConfig['type'], ['annotation', 'staticphp', 'attribute'])) { $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingObjectDefaultName(); } else { $bundleConfig['dir'] = $bundleDir.'/'.$this->getMappingResourceConfigDirectory(); @@ -172,8 +190,7 @@ protected function getMappingDriverBundleConfigDefaults(array $bundleConfig, \Re /** * Register all the collected mapping information with the object manager by registering the appropriate mapping drivers. * - * @param array $objectManager - * @param ContainerBuilder $container A ContainerBuilder instance + * @param array $objectManager */ protected function registerMappingDrivers($objectManager, ContainerBuilder $container) { @@ -181,7 +198,7 @@ protected function registerMappingDrivers($objectManager, ContainerBuilder $cont if ($container->hasDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver'))) { $chainDriverDef = $container->getDefinition($this->getObjectManagerElementName($objectManager['name'].'_metadata_driver')); } else { - $chainDriverDef = new Definition('%'.$this->getObjectManagerElementName('metadata.driver_chain.class%')); + $chainDriverDef = new Definition($this->getMetadataDriverClass('driver_chain')); $chainDriverDef->setPublic(false); } @@ -196,18 +213,22 @@ protected function registerMappingDrivers($objectManager, ContainerBuilder $cont $args[0] = array_merge(array_values($driverPaths), $args[0]); } $mappingDriverDef->setArguments($args); + } elseif ('attribute' === $driverType) { + $mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [ + array_values($driverPaths), + ]); } elseif ('annotation' == $driverType) { - $mappingDriverDef = new Definition('%'.$this->getObjectManagerElementName('metadata.'.$driverType.'.class%'), [ + $mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [ new Reference($this->getObjectManagerElementName('metadata.annotation_reader')), array_values($driverPaths), ]); } else { - $mappingDriverDef = new Definition('%'.$this->getObjectManagerElementName('metadata.'.$driverType.'.class%'), [ + $mappingDriverDef = new Definition($this->getMetadataDriverClass($driverType), [ array_values($driverPaths), ]); } $mappingDriverDef->setPublic(false); - if (false !== strpos($mappingDriverDef->getClass(), 'yml') || false !== strpos($mappingDriverDef->getClass(), 'xml')) { + if (str_contains($mappingDriverDef->getClass(), 'yml') || str_contains($mappingDriverDef->getClass(), 'xml')) { $mappingDriverDef->setArguments([array_flip($driverPaths)]); $mappingDriverDef->addMethodCall('setGlobalBasename', ['mapping']); } @@ -225,7 +246,6 @@ protected function registerMappingDrivers($objectManager, ContainerBuilder $cont /** * Assertion if the specified mapping information is valid. * - * @param array $mappingConfig * @param string $objectManagerName * * @throws \InvalidArgumentException @@ -240,20 +260,15 @@ protected function assertValidMappingConfiguration(array $mappingConfig, $object throw new \InvalidArgumentException(sprintf('Specified non-existing directory "%s" as Doctrine mapping source.', $mappingConfig['dir'])); } - if (!\in_array($mappingConfig['type'], ['xml', 'yml', 'annotation', 'php', 'staticphp'])) { - throw new \InvalidArgumentException(sprintf('Can only configure "xml", "yml", "annotation", "php" or '. - '"staticphp" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. '. - 'You can register them by adding a new driver to the '. - '"%s" service definition.', $this->getObjectManagerElementName($objectManagerName.'_metadata_driver') - )); + if (!\in_array($mappingConfig['type'], ['xml', 'yml', 'annotation', 'php', 'staticphp', 'attribute'])) { + throw new \InvalidArgumentException(sprintf('Can only configure "xml", "yml", "annotation", "php", "staticphp" or "attribute" through the DoctrineBundle. Use your own bundle to configure other metadata drivers. You can register them by adding a new driver to the "%s" service definition.', $this->getObjectManagerElementName($objectManagerName.'_metadata_driver'))); } } /** * Detects what metadata driver to use for the supplied directory. * - * @param string $dir A directory path - * @param ContainerBuilder $container A ContainerBuilder instance + * @param string $dir A directory path * * @return string|null A metadata driver short name, if one can be detected */ @@ -262,11 +277,11 @@ protected function detectMetadataDriver($dir, ContainerBuilder $container) $configPath = $this->getMappingResourceConfigDirectory(); $extension = $this->getMappingResourceExtension(); - if (glob($dir.'/'.$configPath.'/*.'.$extension.'.xml')) { + if (glob($dir.'/'.$configPath.'/*.'.$extension.'.xml', \GLOB_NOSORT)) { $driver = 'xml'; - } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.yml')) { + } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.yml', \GLOB_NOSORT)) { $driver = 'yml'; - } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.php')) { + } elseif (glob($dir.'/'.$configPath.'/*.'.$extension.'.php', \GLOB_NOSORT)) { $driver = 'php'; } else { // add the closest existing directory as a resource @@ -286,9 +301,8 @@ protected function detectMetadataDriver($dir, ContainerBuilder $container) /** * Loads a configured object manager metadata, query or result cache driver. * - * @param array $objectManager A configured object manager - * @param ContainerBuilder $container A ContainerBuilder instance - * @param string $cacheName + * @param array $objectManager A configured object manager + * @param string $cacheName * * @throws \InvalidArgumentException in case of unknown driver type */ @@ -300,10 +314,9 @@ protected function loadObjectManagerCacheDriver(array $objectManager, ContainerB /** * Loads a cache driver. * - * @param string $cacheName The cache driver name - * @param string $objectManagerName The object manager name - * @param array $cacheDriver The cache driver mapping - * @param ContainerBuilder $container The ContainerBuilder instance + * @param string $cacheName The cache driver name + * @param string $objectManagerName The object manager name + * @param array $cacheDriver The cache driver mapping * * @return string * @@ -332,7 +345,7 @@ protected function loadCacheDriver($cacheName, $objectManagerName, array $cacheD $container->setDefinition($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)), $memcachedInstance); $cacheDef->addMethodCall('setMemcached', [new Reference($this->getObjectManagerElementName(sprintf('%s_memcached_instance', $objectManagerName)))]); break; - case 'redis': + case 'redis': $redisClass = !empty($cacheDriver['class']) ? $cacheDriver['class'] : '%'.$this->getObjectManagerElementName('cache.redis.class').'%'; $redisInstanceClass = !empty($cacheDriver['instance_class']) ? $cacheDriver['instance_class'] : '%'.$this->getObjectManagerElementName('cache.redis_instance.class').'%'; $redisHost = !empty($cacheDriver['host']) ? $cacheDriver['host'] : '%'.$this->getObjectManagerElementName('cache.redis_host').'%'; @@ -441,14 +454,22 @@ abstract protected function getMappingResourceConfigDirectory(); */ abstract protected function getMappingResourceExtension(); + /** + * The class name used by the various mapping drivers. + */ + protected function getMetadataDriverClass(string $driverType): string + { + @trigger_error(sprintf('Not declaring the "%s" method in class "%s" is deprecated since Symfony 4.4. This method will be abstract in Symfony 5.0.', __METHOD__, static::class), \E_USER_DEPRECATED); + + return '%'.$this->getObjectManagerElementName('metadata.'.$driverType.'.class').'%'; + } + /** * Search for a manager that is declared as 'auto_mapping' = true. * - * @return string|null The name of the manager. If no one manager is found, returns null - * * @throws \LogicException */ - private function validateAutoMapping(array $managerConfigs) + private function validateAutoMapping(array $managerConfigs): ?string { $autoMappedManager = null; foreach ($managerConfigs as $name => $manager) { @@ -457,7 +478,7 @@ private function validateAutoMapping(array $managerConfigs) } if (null !== $autoMappedManager) { - throw new \LogicException(sprintf('You cannot enable "auto_mapping" on more than one manager at the same time (found in "%s" and %s").', $autoMappedManager, $name)); + throw new \LogicException(sprintf('You cannot enable "auto_mapping" on more than one manager at the same time (found in "%s" and "%s"").', $autoMappedManager, $name)); } $autoMappedManager = $name; diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php index b0db71c929366..25776641796fe 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/DoctrineValidationPass.php @@ -48,11 +48,10 @@ private function updateValidatorMappingFiles(ContainerBuilder $container, string } $files = $container->getParameter('validator.mapping.loader.'.$mapping.'_files_loader.mapping_files'); - $validationPath = 'Resources/config/validation.'.$this->managerType.'.'.$extension; + $validationPath = '/config/validation.'.$this->managerType.'.'.$extension; - foreach ($container->getParameter('kernel.bundles') as $bundle) { - $reflection = new \ReflectionClass($bundle); - if ($container->fileExists($file = \dirname($reflection->getFileName()).'/'.$validationPath)) { + foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) { + if ($container->fileExists($file = $bundle['path'].'/Resources'.$validationPath) || $container->fileExists($file = $bundle['path'].$validationPath)) { $files[] = $file; } } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php index deaa64e7c9084..61046c28a5098 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPass.php @@ -11,6 +11,8 @@ namespace Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass; +use Symfony\Bridge\Doctrine\ContainerAwareEventManager; +use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -55,36 +57,62 @@ public function process(ContainerBuilder $container) } $this->connections = $container->getParameter($this->connections); - $this->addTaggedSubscribers($container); - $this->addTaggedListeners($container); + $listenerRefs = []; + $this->addTaggedSubscribers($container, $listenerRefs); + $this->addTaggedListeners($container, $listenerRefs); + + // replace service container argument of event managers with smaller service locator + // so services can even remain private + foreach ($listenerRefs as $connection => $refs) { + $this->getEventManagerDef($container, $connection) + ->replaceArgument(0, ServiceLocatorTagPass::register($container, $refs)); + } } - private function addTaggedSubscribers(ContainerBuilder $container) + private function addTaggedSubscribers(ContainerBuilder $container, array &$listenerRefs) { $subscriberTag = $this->tagPrefix.'.event_subscriber'; $taggedSubscribers = $this->findAndSortTags($subscriberTag, $container); + $managerDefs = []; foreach ($taggedSubscribers as $taggedSubscriber) { - list($id, $tag) = $taggedSubscriber; + [$id, $tag] = $taggedSubscriber; $connections = isset($tag['connection']) ? [$tag['connection']] : array_keys($this->connections); foreach ($connections as $con) { if (!isset($this->connections[$con])) { - throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: %s', $con, $id, implode(', ', array_keys($this->connections)))); + throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: "%s".', $con, $id, implode('", "', array_keys($this->connections)))); } - $this->getEventManagerDef($container, $con)->addMethodCall('addEventSubscriber', [new Reference($id)]); + if (!isset($managerDefs[$con])) { + $managerDef = $parentDef = $this->getEventManagerDef($container, $con); + while (!$parentDef->getClass() && $parentDef instanceof ChildDefinition) { + $parentDef = $container->findDefinition($parentDef->getParent()); + } + $managerClass = $container->getParameterBag()->resolveValue($parentDef->getClass()); + $managerDefs[$con] = [$managerDef, $managerClass]; + } else { + [$managerDef, $managerClass] = $managerDefs[$con]; + } + + if (ContainerAwareEventManager::class === $managerClass) { + $listenerRefs[$con][$id] = new Reference($id); + $refs = $managerDef->getArguments()[1] ?? []; + $refs[] = $id; + $managerDef->setArgument(1, $refs); + } else { + $managerDef->addMethodCall('addEventSubscriber', [new Reference($id)]); + } } } } - private function addTaggedListeners(ContainerBuilder $container) + private function addTaggedListeners(ContainerBuilder $container, array &$listenerRefs) { $listenerTag = $this->tagPrefix.'.event_listener'; $taggedListeners = $this->findAndSortTags($listenerTag, $container); - $listenerRefs = []; foreach ($taggedListeners as $taggedListener) { - list($id, $tag) = $taggedListener; + [$id, $tag] = $taggedListener; if (!isset($tag['event'])) { throw new InvalidArgumentException(sprintf('Doctrine event listener "%s" must specify the "event" attribute.', $id)); } @@ -92,7 +120,7 @@ private function addTaggedListeners(ContainerBuilder $container) $connections = isset($tag['connection']) ? [$tag['connection']] : array_keys($this->connections); foreach ($connections as $con) { if (!isset($this->connections[$con])) { - throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: %s', $con, $id, implode(', ', array_keys($this->connections)))); + throw new RuntimeException(sprintf('The Doctrine connection "%s" referenced in service "%s" does not exist. Available connections names: "%s".', $con, $id, implode('", "', array_keys($this->connections)))); } $listenerRefs[$con][$id] = new Reference($id); @@ -100,16 +128,9 @@ private function addTaggedListeners(ContainerBuilder $container) $this->getEventManagerDef($container, $con)->addMethodCall('addEventListener', [[$tag['event']], $id]); } } - - // replace service container argument of event managers with smaller service locator - // so services can even remain private - foreach ($listenerRefs as $connection => $refs) { - $this->getEventManagerDef($container, $connection) - ->replaceArgument(0, ServiceLocatorTagPass::register($container, $refs)); - } } - private function getEventManagerDef(ContainerBuilder $container, $name) + private function getEventManagerDef(ContainerBuilder $container, string $name) { if (!isset($this->eventManagers[$name])) { $this->eventManagers[$name] = $container->getDefinition(sprintf($this->managerTemplate, $name)); @@ -125,21 +146,16 @@ private function getEventManagerDef(ContainerBuilder $container, $name) * and knowing that the \SplPriorityQueue class does not respect the FIFO method, * we should not use this class. * - * @see https://bugs.php.net/bug.php?id=53710 - * @see https://bugs.php.net/bug.php?id=60926 - * - * @param string $tagName - * @param ContainerBuilder $container - * - * @return array + * @see https://bugs.php.net/53710 + * @see https://bugs.php.net/60926 */ - private function findAndSortTags($tagName, ContainerBuilder $container) + private function findAndSortTags(string $tagName, ContainerBuilder $container): array { $sortedTags = []; foreach ($container->findTaggedServiceIds($tagName, true) as $serviceId => $tags) { foreach ($tags as $attributes) { - $priority = isset($attributes['priority']) ? $attributes['priority'] : 0; + $priority = $attributes['priority'] ?? 0; $sortedTags[$priority][] = [$serviceId, $attributes]; } } diff --git a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php index 5b1d78fbf82c8..e253720d8026f 100644 --- a/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php +++ b/src/Symfony/Bridge/Doctrine/DependencyInjection/CompilerPass/RegisterMappingsPass.php @@ -125,7 +125,7 @@ public function __construct($driver, array $namespaces, array $managerParameters $this->driverPattern = $driverPattern; $this->enabledParameter = $enabledParameter; if (\count($aliasMap) && (!$configurationPattern || !$registerAliasMethodName)) { - throw new \InvalidArgumentException('configurationPattern and registerAliasMethodName are required to register namespace alias'); + throw new \InvalidArgumentException('configurationPattern and registerAliasMethodName are required to register namespace alias.'); } $this->configurationPattern = $configurationPattern; $this->registerAliasMethodName = $registerAliasMethodName; @@ -143,7 +143,7 @@ public function process(ContainerBuilder $container) $mappingDriverDef = $this->getDriver($container); $chainDriverDefService = $this->getChainDriverServiceName($container); - // Definition for a Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain + // Definition for a Doctrine\Persistence\Mapping\Driver\MappingDriverChain $chainDriverDef = $container->getDefinition($chainDriverDefService); foreach ($this->namespaces as $namespace) { $chainDriverDef->addMethodCall('addDriver', [$mappingDriverDef, $namespace]); @@ -191,12 +191,10 @@ protected function getDriver(ContainerBuilder $container) /** * Get the service name from the pattern and the configured manager name. * - * @return string a service definition name - * * @throws InvalidArgumentException if none of the managerParameters has a * non-empty value */ - private function getConfigurationServiceName(ContainerBuilder $container) + private function getConfigurationServiceName(ContainerBuilder $container): string { return sprintf($this->configurationPattern, $this->getManagerName($container)); } @@ -207,11 +205,9 @@ private function getConfigurationServiceName(ContainerBuilder $container) * The default implementation loops over the managerParameters and returns * the first non-empty parameter. * - * @return string The name of the active manager - * * @throws InvalidArgumentException if none of the managerParameters is found in the container */ - private function getManagerName(ContainerBuilder $container) + private function getManagerName(ContainerBuilder $container): string { foreach ($this->managerParameters as $param) { if ($container->hasParameter($param)) { @@ -222,10 +218,7 @@ private function getManagerName(ContainerBuilder $container) } } - throw new InvalidArgumentException(sprintf( - 'Could not find the manager name parameter in the container. Tried the following parameter names: "%s"', - implode('", "', $this->managerParameters) - )); + th 9E88 row new InvalidArgumentException(sprintf('Could not find the manager name parameter in the container. Tried the following parameter names: "%s".', implode('", "', $this->managerParameters))); } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index cd040d12a9b03..0a5ea326eff0a 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -11,9 +11,8 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; -use Symfony\Component\Form\ChoiceList\ChoiceListInterface; use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; /** @@ -29,9 +28,9 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface private $objectLoader; /** - * @var ChoiceListInterface + * @var array|null */ - private $choiceList; + private $choices; /** * Creates a new choice loader. @@ -40,17 +39,14 @@ class DoctrineChoiceLoader implements ChoiceLoaderInterface * passed which optimizes the object loading for one of the Doctrine * mapper implementations. * - * @param ObjectManager $manager The object manager - * @param string $class The class name of the loaded objects - * @param IdReader|null $idReader The reader for the object IDs - * @param EntityLoaderInterface|null $objectLoader The objects loader + * @param string $class The class name of the loaded objects */ public function __construct(ObjectManager $manager, string $class, IdReader $idReader = null, EntityLoaderInterface $objectLoader = null) { $classMetadata = $manager->getClassMetadata($class); if ($idReader && !$idReader->isSingleId()) { - @trigger_error(sprintf('Passing an instance of "%s" to "%s" with an entity class "%s" that has a composite id is deprecated since Symfony 4.3 and will throw an exception in 5.0.', IdReader::class, __CLASS__, $class), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing an instance of "%s" to "%s" with an entity class "%s" that has a composite id is deprecated since Symfony 4.3 and will throw an exception in 5.0.', IdReader::class, __CLASS__, $class), \E_USER_DEPRECATED); // In Symfony 5.0 // throw new \InvalidArgumentException(sprintf('The second argument `$idReader` of "%s" must be null when the query cannot be optimized because of composite id fields.', __METHOD__)); @@ -60,7 +56,7 @@ public function __construct(ObjectManager $manager, string $class, IdReader $idR $idReader = new IdReader($manager, $classMetadata); if ($idReader->isSingleId()) { - @trigger_error(sprintf('Not explicitly passing an instance of "%s" to "%s" when it can optimize single id entity "%s" has been deprecated in 4.3 and will not apply any optimization in 5.0.', IdReader::class, __CLASS__, $class), E_USER_DEPRECATED); + @trigger_error(sprintf('Not explicitly passing an instance of "%s" to "%s" when it can optimize single id entity "%s" has been deprecated in 4.3 and will not apply any optimization in 5.0.', IdReader::class, __CLASS__, $class), \E_USER_DEPRECATED); } else { $idReader = null; } @@ -77,15 +73,13 @@ public function __construct(ObjectManager $manager, string $class, IdReader $idR */ public function loadChoiceList($value = null) { - if ($this->choiceList) { - return $this->choiceList; + if (null === $this->choices) { + $this->choices = $this->objectLoader + ? $this->objectLoader->getEntities() + : $this->manager->getRepository($this->class)->findAll(); } - $objects = $this->objectLoader - ? $this->objectLoader->getEntities() - : $this->manager->getRepository($this->class)->findAll(); - - return $this->choiceList = new ArrayChoiceList($objects, $value); + return new ArrayChoiceList($this->choices, $value); } /** @@ -103,7 +97,7 @@ public function loadValuesForChoices(array $choices, $value = null) $optimize = $this->idReader && (null === $value || \is_array($value) && $value[0] === $this->idReader); // Attention: This optimization does not check choices for existence - if ($optimize && !$this->choiceList && $this->idReader->isSingleId()) { + if ($optimize && !$this->choices && $this->idReader->isSingleId()) { $values = []; // Maintain order and indices of the given objects @@ -139,7 +133,7 @@ public function loadChoicesForValues(array $values, $value = null) // a single-field identifier $optimize = $this->idReader && (null === $value || \is_array($value) && $this->idReader === $value[0]); - if ($optimize && !$this->choiceList && $this->objectLoader && $this->idReader->isSingleId()) { + if ($optimize && !$this->choices && $this->objectLoader && $this->idReader->isSingleId()) { $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); $objectsById = []; $objects = []; diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index 3509d9b03b329..f56193e81062f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -11,8 +11,8 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; use Symfony\Component\Form\Exception\RuntimeException; /** @@ -91,7 +91,7 @@ public function isIntId(): bool public function getIdValue($object) { if (!$object) { - return; + return null; } if (!$this->om->contains($object)) { diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php index 96f5e2f5f1868..e53fa3366e953 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/ORMQueryBuilderLoader.php @@ -32,11 +32,6 @@ class ORMQueryBuilderLoader implements EntityLoaderInterface */ private $queryBuilder; - /** - * Construct an ORM Query Builder Loader. - * - * @param QueryBuilder $queryBuilder The query builder for creating the query builder - */ public function __construct(QueryBuilder $queryBuilder) { $this->queryBuilder = $queryBuilder; @@ -55,6 +50,21 @@ public function getEntities() */ public function getEntitiesByIds($identifier, array $values) { + if (null !== $this->queryBuilder->getMaxResults() || 0 < (int) $this->queryBuilder->getFirstResult()) { + // an offset or a limit would apply on results including the where clause with submitted id values + // that could make invalid choices valid + $choices = []; + $metadata = $this->queryBuilder->getEntityManager()->getClassMetadata(current($this->queryBuilder->getRootEntities())); + + foreach ($this->getEntities() as $entity) { + if (\in_array((string) current($metadata->getIdentifierValues($entity)), $values, true)) { + $choices[] = $entity; + } + } + + return $choices; + } + $qb = clone $this->queryBuilder; $alias = current($qb->getRootAliases()); $parameter = 'ORMQueryBuilderLoader_getEntitiesByIds_'.$identifier; diff --git a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php index 3202dae97f5c2..d8235a681479f 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php +++ b/src/Symfony/Bridge/Doctrine/Form/DataTransformer/CollectionToArrayTransformer.php @@ -24,8 +24,6 @@ class CollectionToArrayTransformer implements DataTransformerInterface /** * Transforms a collection into an array. * - * @return mixed An array of entities - * * @throws TransformationFailedException */ public function transform($collection) @@ -48,11 +46,9 @@ public function transform($collection) } /** - * Transforms choice keys into entities. - * - * @param mixed $array An array of entities + * Transforms an array into a collection. * - * @return Collection A collection of entities + * @return Collection */ public function reverseTransform($array) { diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php index 891754a1da08f..c2897c6d9aba8 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmExtension.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\Doctrine\Form; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\Type\EntityType; use Symfony\Component\Form\AbstractExtension; diff --git a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php index 34fb04aed283e..944c305ab70a7 100644 --- a/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php +++ b/src/Symfony/Bridge/Doctrine/Form/DoctrineOrmTypeGuesser.php @@ -11,12 +11,13 @@ namespace Symfony\Bridge\Doctrine\Form; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\Mapping\MappingException; -use Doctrine\Common\Persistence\Proxy; use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as LegacyMappingException; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\MappingException; +use Doctrine\Persistence\Proxy; use Symfony\Component\Form\FormTypeGuesserInterface; use Symfony\Component\Form\Guess\Guess; use Symfony\Component\Form\Guess\TypeGuess; @@ -28,9 +29,15 @@ class DoctrineOrmTypeGuesser implements FormTypeGuesserInterface private $cache = []; + private static $useDeprecatedConstants; + public function __construct(ManagerRegistry $registry) { $this->registry = $registry; + + if (null === self::$useDeprecatedConstants) { + self::$useDeprecatedConstants = !class_exists(Types::class); + } } /** @@ -42,7 +49,7 @@ public function guessType($class, $property) return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); } - list($metadata, $name) = $ret; + [$metadata, $name] = $ret; if ($metadata->hasAssociation($property)) { $multiple = $metadata->isCollectionValuedAssociation($property); @@ -52,13 +59,16 @@ public function guessType($class, $property) } switch ($metadata->getTypeOfField($property)) { - case Type::TARRAY: - case Type::SIMPLE_ARRAY: + case self::$useDeprecatedConstants ? Type::TARRAY : Types::ARRAY: + // no break + case self::$useDeprecatedConstants ? Type::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CollectionType', [], Guess::MEDIUM_CONFIDENCE); - case Type::BOOLEAN: + case self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\CheckboxType', [], Guess::HIGH_CONFIDENCE); - case Type::DATETIME: - case Type::DATETIMETZ: + case self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE: + // no break + case self::$useDeprecatedConstants ? Type::DATETIMETZ : Types::DATETIMETZ_MUTABLE: + // no break case 'vardatetime': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', [], Guess::HIGH_CONFIDENCE); case 'datetime_immutable': @@ -66,25 +76,27 @@ public function guessType($class, $property) return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateTimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); case 'dateinterval': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateIntervalType', [], Guess::HIGH_CONFIDENCE); - case Type::DATE: + case self::$useDeprecatedConstants ? Type::DATE : Types::DATE_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::HIGH_CONFIDENCE); case 'date_immutable': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case Type::TIME: + case self::$useDeprecatedConstants ? Type::TIME : Types::TIME_MUTABLE: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', [], Guess::HIGH_CONFIDENCE); case 'time_immutable': return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TimeType', ['input' => 'datetime_immutable'], Guess::HIGH_CONFIDENCE); - case Type::DECIMAL: + case self::$useDeprecatedConstants ? Type::DECIMAL : Types::DECIMAL: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', ['input' => 'string'], Guess::MEDIUM_CONFIDENCE); - case Type::FLOAT: + case self::$useDeprecatedConstants ? Type::FLOAT : Types::FLOAT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\NumberType', [], Guess::MEDIUM_CONFIDENCE); - case Type::INTEGER: - case Type::BIGINT: - case Type::SMALLINT: + case self::$useDeprecatedConstants ? Type::INTEGER : Types::INTEGER: + // no break + case self::$useDeprecatedConstants ? Type::BIGINT : Types::BIGINT: + // no break + case self::$useDeprecatedConstants ? Type::SMALLINT : Types::SMALLINT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE); - case Type::STRING: + case self::$useDeprecatedConstants ? Type::STRING : Types::STRING: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::MEDIUM_CONFIDENCE); - case Type::TEXT: + case self::$useDeprecatedConstants ? Type::TEXT : Types::TEXT: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextareaType', [], Guess::MEDIUM_CONFIDENCE); default: return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\TextType', [], Guess::LOW_CONFIDENCE); @@ -99,7 +111,7 @@ public function guessRequired($class, $property) $classMetadatas = $this->getMetadata($class); if (!$classMetadatas) { - return; + return null; } /** @var ClassMetadataInfo $classMetadata */ @@ -107,7 +119,7 @@ public function guessRequired($class, $property) // Check whether the field exists and is nullable or not if (isset($classMetadata->fieldMappings[$property])) { - if (!$classMetadata->isNullable($property) && Type::BOOLEAN !== $classMetadata->getTypeOfField($property)) { + if (!$classMetadata->isNullable($property) && (self::$useDeprecatedConstants ? Type::BOOLEAN : Types::BOOLEAN) !== $classMetadata->getTypeOfField($property)) { return new ValueGuess(true, Guess::HIGH_CONFIDENCE); } @@ -127,6 +139,8 @@ public function guessRequired($class, $property) return new ValueGuess(!$mapping['joinColumns'][0]['nullable'], Guess::HIGH_CONFIDENCE); } + + return null; } /** @@ -142,10 +156,12 @@ public function guessMaxLength($class, $property) return new ValueGuess($mapping['length'], Guess::HIGH_CONFIDENCE); } - if (\in_array($ret[0]->getTypeOfField($property), [Type::DECIMAL, Type::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } + + return null; } /** @@ -155,10 +171,12 @@ public function guessPattern($class, $property) { $ret = $this->getMetadata($class); if ($ret && isset($ret[0]->fieldMappings[$property]) && !$ret[0]->hasAssociation($property)) { - if (\in_array($ret[0]->getTypeOfField($property), [Type::DECIMAL, Type::FLOAT])) { + if (\in_array($ret[0]->getTypeOfField($property), self::$useDeprecatedConstants ? [Type::DECIMAL, Type::FLOAT] : [Types::DECIMAL, Types::FLOAT])) { return new ValueGuess(null, Guess::MEDIUM_CONFIDENCE); } } + + return null; } protected function getMetadata($class) @@ -180,6 +198,8 @@ protected function getMetadata($class) // not an entity or mapped super class, using Doctrine ORM 2.2 } } + + return null; } private static function getRealClass(string $class): string diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 88f9cf9101c7d..3a77e74c560b0 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -12,8 +12,8 @@ namespace Symfony\Bridge\Doctrine\Form\Type; use Doctrine\Common\Collections\Collection; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\ObjectManager; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; @@ -22,6 +22,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -51,12 +52,10 @@ abstract class DoctrineType extends AbstractType implements ResetInterface * * @param object $choice The object * - * @return string The string representation of the object - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public static function createChoiceLabel($choice) + public static function createChoiceLabel($choice): string { return (string) $choice; } @@ -73,12 +72,10 @@ public static function createChoiceLabel($choice) * @param string $value The choice value. Corresponds to the object's * ID here. * - * @return string The field name - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public static function createChoiceName($choice, $key, $value) + public static function createChoiceName($choice, $key, $value): string { return str_replace('-', '_', (string) $value); } @@ -88,17 +85,18 @@ public static function createChoiceName($choice, $key, $value) * For instance in ORM two query builders with an equal SQL string and * equal parameters are considered to be equal. * - * @param object $queryBuilder + * @param object $queryBuilder A query builder, type declaration is not present here as there + * is no common base class for the different implementations * - * @return array|false Array with important QueryBuilder parts or false if - * they can't be determined + * @return array|null Array with important QueryBuilder parts or null if + * they can't be determined * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash($queryBuilder) + public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array { - return false; + return null; } public function __construct(ManagerRegistry $registry) @@ -127,7 +125,7 @@ public function configureOptions(OptionsResolver $resolver) // If there is no QueryBuilder we can safely cache DoctrineChoiceLoader, // also if concrete Type can return important QueryBuilder parts to generate // hash key we go for it as well - if (!$options['query_builder'] || false !== ($qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder']))) { + if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) { $hash = CachingFactoryDecorator::generateHash([ $options['em'], $options['class'], @@ -160,6 +158,8 @@ public function configureOptions(OptionsResolver $resolver) return $doctrineChoiceLoader; } + + return null; }; $choiceName = function (Options $options) { @@ -171,6 +171,7 @@ public function configureOptions(OptionsResolver $resolver) } // Otherwise, an incrementing integer is used as name automatically + return null; }; // The choices are always indexed by ID (see "choices" normalizer @@ -184,10 +185,10 @@ public function configureOptions(OptionsResolver $resolver) } // Otherwise, an incrementing integer is used as value automatically + return null; }; $emNormalizer = function (Options $options, $em) { - /* @var ManagerRegistry $registry */ if (null !== $em) { if ($em instanceof ObjectManager) { return $em; @@ -259,15 +260,14 @@ public function configureOptions(OptionsResolver $resolver) $resolver->setNormalizer('query_builder', $queryBuilderNormalizer); $resolver->setNormalizer('id_reader', $idReaderNormalizer); - $resolver->setAllowedTypes('em', ['null', 'string', 'Doctrine\Common\Persistence\ObjectManager']); + $resolver->setAllowedTypes('em', ['null', 'string', ObjectManager::class]); } /** * Return the default loader object. * - * @param ObjectManager $manager - * @param mixed $queryBuilder - * @param string $class + * @param mixed $queryBuilder + * @param string $class * * @return EntityLoaderInterface */ @@ -275,11 +275,14 @@ abstract public function getLoader(ObjectManager $manager, $queryBuilder, $class public function getParent() { - return 'Symfony\Component\Form\Extension\Core\Type\ChoiceType'; + return ChoiceType::class; } public function reset() { + $this->idReaders = []; $this->choiceLoaders = []; } } + +interface_exists(ObjectManager::class); diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index b6c598350c0a8..69c92c0b08389 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -11,9 +11,9 @@ namespace Symfony\Bridge\Doctrine\Form\Type; -use Doctrine\Common\Persistence\ObjectManager; use Doctrine\ORM\Query\Parameter; use Doctrine\ORM\QueryBuilder; +use Doctrine\Persistence\ObjectManager; use Symfony\Bridge\Doctrine\Form\ChoiceList\ORMQueryBuilderLoader; use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\OptionsResolver\Options; @@ -46,14 +46,17 @@ public function configureOptions(OptionsResolver $resolver) /** * Return the default loader object. * - * @param ObjectManager $manager - * @param QueryBuilder $queryBuilder - * @param string $class + * @param QueryBuilder $queryBuilder + * @param string $class * * @return ORMQueryBuilderLoader */ public function getLoader(ObjectManager $manager, $queryBuilder, $class) { + if (!$queryBuilder instanceof QueryBuilder) { + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + } + return new ORMQueryBuilderLoader($queryBuilder); } @@ -71,13 +74,15 @@ public function getBlockPrefix() * * @param QueryBuilder $queryBuilder * - * @return array - * * @internal This method is public to be usable as callback. It should not * be used in user code. */ - public function getQueryBuilderPartsForCachingHash($queryBuilder) + public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array { + if (!$queryBuilder instanceof QueryBuilder) { + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + } + return [ $queryBuilder->getQuery()->getSQL(), array_map([$this, 'parameterToArray'], $queryBuilder->getParameters()->toArray()), @@ -86,11 +91,11 @@ public function getQueryBuilderPartsForCachingHash($queryBuilder) /** * Converts a query parameter to an array. - * - * @return array The array representation of the parameter */ - private function parameterToArray(Parameter $parameter) + private function parameterToArray(Parameter $parameter): array { return [$parameter->getName(), $parameter->getType(), $parameter->getValue()]; } } + +interface_exists(ObjectManager::class); diff --git a/src/Symfony/Bridge/Doctrine/LICENSE b/src/Symfony/Bridge/Doctrine/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bridge/Doctrine/LICENSE +++ b/src/Symfony/Bridge/Doctrine/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php b/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php index 63880a6d614a0..f7d2ae00e5df9 100644 --- a/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php +++ b/src/Symfony/Bridge/Doctrine/Logger/DbalLogger.php @@ -20,8 +20,8 @@ */ class DbalLogger implements SQLLogger { - const MAX_STRING_LENGTH = 32; - const BINARY_DATA_VALUE = '(binary value)'; + public const MAX_STRING_LENGTH = 32; + public const BINARY_DATA_VALUE = '(binary value)'; protected $logger; protected $stopwatch; @@ -34,6 +34,8 @@ public function __construct(LoggerInterface $logger = null, Stopwatch $stopwatch /** * {@inheritdoc} + * + * @return void */ public function startQuery($sql, array $params = null, array $types = null) { @@ -48,6 +50,8 @@ public function startQuery($sql, array $params = null, array $types = null) /** * {@inheritdoc} + * + * @return void */ public function stopQuery() { @@ -67,7 +71,7 @@ protected function log($message, array $params) $this->logger->debug($message, $params); } - private function normalizeParams(array $params) + private function normalizeParams(array $params): array { foreach ($params as $index => $param) { // normalize recursively diff --git a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php index ae481b572628e..b001cfd933210 100644 --- a/src/Symfony/Bridge/Doctrine/ManagerRegistry.php +++ b/src/Symfony/Bridge/Doctrine/ManagerRegistry.php @@ -11,14 +11,16 @@ namespace Symfony\Bridge\Doctrine; -use Doctrine\Common\Persistence\AbstractManagerRegistry; +use Doctrine\Persistence\AbstractManagerRegistry; +use ProxyManager\Proxy\GhostObjectInterface; use ProxyManager\Proxy\LazyLoadingInterface; +use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\DependencyInjection\Container; /** * References Doctrine connections and entity/document managers. * - * @author Lukas Kahwe Smith + * @author Lukas Kahwe Smith */ abstract class ManagerRegistry extends AbstractManagerRegistry { @@ -29,6 +31,8 @@ abstract class ManagerRegistry extends AbstractManagerRegistry /** * {@inheritdoc} + * + * @return object */ protected function getService($name) { @@ -37,6 +41,8 @@ protected function getService($name) /** * {@inheritdoc} + * + * @return void */ protected function resetService($name) { @@ -46,7 +52,10 @@ protected function resetService($name) $manager = $this->container->get($name); if (!$manager instanceof LazyLoadingInterface) { - throw new \LogicException('Resetting a non-lazy manager service is not supported. '.(interface_exists(LazyLoadingInterface::class) ? sprintf('Declare the "%s" service as lazy.', $name) : 'Try running "composer require symfony/proxy-manager-bridge".')); + throw new \LogicException('Resetting a non-lazy manager service is not supported. '.(interface_exists(LazyLoadingInterface::class) && class_exists(RuntimeInstantiator::class) ? sprintf('Declare the "%s" service as lazy.', $name) : 'Try running "composer require symfony/proxy-manager-bridge".')); + } + if ($manager instanceof GhostObjectInterface) { + throw new \LogicException('Resetting a lazy-ghost-object manager service is not supported.'); } $manager->setProxyInitializer(\Closure::bind( function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { @@ -57,7 +66,7 @@ function (&$wrappedInstance, LazyLoadingInterface $manager) use ($name) { $name = $this->aliases[$name]; } if (isset($this->fileMap[$name])) { - $wrappedInstance = $this->load($this->fileMap[$name]); + $wrappedInstance = $this->load($this->fileMap[$name], false); } else { $method = $this->methodMap[$name] ?? 'get'.strtr($name, $this->underscoreMap).'Service'; // BC with DI v3.4 $wrappedInstance = $this->{$method}(false); diff --git a/src/Symfony/Bridge/Doctrine/Messenger/AbstractDoctrineMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/AbstractDoctrineMiddleware.php index a29c95af3da03..9fbf2deb963e3 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/AbstractDoctrineMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/AbstractDoctrineMiddleware.php @@ -11,8 +11,8 @@ namespace Symfony\Bridge\Doctrine\Messenger; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; use Symfony\Component\Messenger\Middleware\MiddlewareInterface; diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerMiddleware.php deleted file mode 100644 index bb0782232ee38..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerMiddleware.php +++ /dev/null @@ -1,33 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Doctrine\Messenger; - -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Middleware\StackInterface; - -/** - * Clears entity manager after calling all handlers. - * - * @author Konstantin Myakshin - */ -class DoctrineClearEntityManagerMiddleware extends AbstractDoctrineMiddleware -{ - protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope - { - try { - return $stack->next()->handle($envelope, $stack); - } finally { - $entityManager->clear(); - } - } -} diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.php new file mode 100644 index 0000000000000..d702186a713ce --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineClearEntityManagerWorkerSubscriber.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\Bridge\Doctrine\Messenger; + +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent; +use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent; + +/** + * Clears entity managers between messages being handled to avoid outdated data. + * + * @author Ryan Weaver + */ +class DoctrineClearEntityManagerWorkerSubscriber implements EventSubscriberInterface +{ + private $managerRegistry; + + public function __construct(ManagerRegistry $managerRegistry) + { + $this->managerRegistry = $managerRegistry; + } + + public function onWorkerMessageHandled() + { + $this->clearEntityManagers(); + } + + public function onWorkerMessageFailed() + { + $this->clearEntityManagers(); + } + + public static function getSubscribedEvents() + { + yield WorkerMessageHandledEvent::class => 'onWorkerMessageHandled'; + yield WorkerMessageFailedEvent::class => 'onWorkerMessageFailed'; + } + + private function clearEntityManagers() + { + foreach ($this->managerRegistry->getManagers() as $manager) { + $manager->clear(); + } + } +} diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineCloseConnectionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineCloseConnectionMiddleware.php index d3db37563f963..b0a96e05daa33 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineCloseConnectionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineCloseConnectionMiddleware.php @@ -14,6 +14,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\StackInterface; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; /** * Closes connection and therefore saves number of connections. @@ -29,7 +30,9 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel return $stack->next()->handle($envelope, $stack); } finally { - $connection->close(); + if (null !== $envelope->last(ConsumedByWorkerStamp::class)) { + $connection->close(); + } } } } diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php index 604190d4aea8d..30f12129c2719 100644 --- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php +++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrinePingConnectionMiddleware.php @@ -11,9 +11,12 @@ namespace Symfony\Bridge\Doctrine\Messenger; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Middleware\StackInterface; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; /** * Checks whether the connection is still open or reconnects otherwise. @@ -23,10 +26,21 @@ class DoctrinePingConnectionMiddleware extends AbstractDoctrineMiddleware { protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope + { + if (null !== $envelope->last(ConsumedByWorkerStamp::class)) { + $this->pingConnection($entityManager); + } + + return $stack->next()->handle($envelope, $stack); + } + + private function pingConnection(EntityManagerInterface $entityManager) { $connection = $entityManager->getConnection(); - if (!$connection->ping()) { + try { + $connection->executeQuery($connection->getDatabasePlatform()->getDummySelectSQL()); + } catch (DBALException|Exception $e) { $connection->close(); $connection->connect(); } @@ -34,7 +48,5 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel if (!$entityManager->isOpen()) { $this->managerRegistry->resetManager($this->entityManagerName); } - - return $stack->next()->handle($envelope, $stack); } } diff --git a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php index f14c38b36252a..4391411a94a76 100644 --- a/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php +++ b/src/Symfony/Bridge/Doctrine/PropertyInfo/DoctrineExtractor.php @@ -11,13 +11,14 @@ namespace Symfony\Bridge\Doctrine\PropertyInfo; -use Doctrine\Common\Persistence\Mapping\ClassMetadataFactory; -use Doctrine\Common\Persistence\Mapping\MappingException; use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as OrmMappingException; +use Doctrine\Persistence\Mapping\ClassMetadataFactory; +use Doctrine\Persistence\Mapping\MappingException; use Symfony\Component\PropertyInfo\PropertyAccessExtractorInterface; use Symfony\Component\PropertyInfo\PropertyListExtractorInterface; use Symfony\Component\PropertyInfo\PropertyTypeExtractorInterface; @@ -33,6 +34,8 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE private $entityManager; private $classMetadataFactory; + private static $useDeprecatedConstants; + /** * @param EntityManagerInterface $entityManager */ @@ -41,11 +44,15 @@ public function F438 __construct($entityManager) if ($entityManager instanceof EntityManagerInterface) { $this->entityManager = $entityManager; } elseif ($entityManager instanceof ClassMetadataFactory) { - @trigger_error(sprintf('Injecting an instance of "%s" in "%s" is deprecated since Symfony 4.2, inject an instance of "%s" instead.', ClassMetadataFactory::class, __CLASS__, EntityManagerInterface::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Injecting an instance of "%s" in "%s" is deprecated since Symfony 4.2, inject an instance of "%s" instead.', ClassMetadataFactory::class, __CLASS__, EntityManagerInterface::class), \E_USER_DEPRECATED); $this->classMetadataFactory = $entityManager; } else { throw new \TypeError(sprintf('$entityManager must be an instance of "%s", "%s" given.', EntityManagerInterface::class, \is_object($entityManager) ? \get_class($entityManager) : \gettype($entityManager))); } + + if (null === self::$useDeprecatedConstants) { + self::$useDeprecatedConstants = !class_exists(Types::class); + } } /** @@ -59,9 +66,9 @@ public function getProperties($class, array $context = []) $properties = array_merge($metadata->getFieldNames(), $metadata->getAssociationNames()); - if ($metadata instanceof ClassMetadataInfo && class_exists('Doctrine\ORM\Mapping\Embedded') && $metadata->embeddedClasses) { + if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && $metadata->embeddedClasses) { $properties = array_filter($properties, function ($property) { - return false === strpos($property, '.'); + return !str_contains($property, '.'); }); $properties = array_merge($properties, array_keys($metadata->embeddedClasses)); @@ -100,21 +107,33 @@ public function getTypes($class, $property, array $context = []) $associationMapping = $metadata->getAssociationMapping($property); if (isset($associationMapping['indexBy'])) { - $indexProperty = $associationMapping['indexBy']; /** @var ClassMetadataInfo $subMetadata */ $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); - $typeOfField = $subMetadata->getTypeOfField($indexProperty); - if (null === $typeOfField) { - $associationMapping = $subMetadata->getAssociationMapping($indexProperty); - - /** @var ClassMetadataInfo $subMetadata */ - $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($indexProperty); - $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); - $typeOfField = $subMetadata->getTypeOfField($indexProperty); + // Check if indexBy value is a property + $fieldName = $associationMapping['indexBy']; + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + $fieldName = $subMetadata->getFieldForColumn($associationMapping['indexBy']); + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($fieldName))) { + // Maybe the column name is the association join column? + $associationMapping = $subMetadata->getAssociationMapping($fieldName); + + /** @var ClassMetadataInfo $subMetadata */ + $indexProperty = $subMetadata->getSingleAssociationReferencedJoinColumnName($fieldName); + $subMetadata = $this->entityManager ? $this->entityManager->getClassMetadata($associationMapping['targetEntity']) : $this->classMetadataFactory->getMetadataFor($associationMapping['targetEntity']); + + // Not a property, maybe a column name? + if (null === ($typeOfField = $subMetadata->getTypeOfField($indexProperty))) { + $fieldName = $subMetadata->getFieldForColumn($indexProperty); + $typeOfField = $subMetadata->getTypeOfField($fieldName); + } + } } - $collectionKeyType = $this->getPhpType($typeOfField); + if (!$collectionKeyType = $this->getPhpType($typeOfField)) { + return null; + } } } @@ -128,46 +147,75 @@ public function getTypes($class, $property, array $context = []) )]; } - if ($metadata instanceof ClassMetadataInfo && class_exists('Doctrine\ORM\Mapping\Embedded') && isset($metadata->embeddedClasses[$property])) { + if ($metadata instanceof ClassMetadataInfo && class_exists(\Doctrine\ORM\Mapping\Embedded::class) && isset($metadata->embeddedClasses[$property])) { return [new Type(Type::BUILTIN_TYPE_OBJECT, false, $metadata->embeddedClasses[$property]['class'])]; } if ($metadata->hasField($property)) { $typeOfField = $metadata->getTypeOfField($property); - $nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property); - - switch ($typeOfField) { - case DBALType::DATE: - case DBALType::DATETIME: - case DBALType::DATETIMETZ: - case 'vardatetime': - case DBALType::TIME: - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; - - case 'date_immutable': - case 'datetime_immutable': - case 'datetimetz_immutable': - case 'time_immutable': - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; - - case 'dateinterval': - return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; - - case DBALType::TARRAY: - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; - case DBALType::SIMPLE_ARRAY: - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]; + if (!$builtinType = $this->getPhpType($typeOfField)) { + return null; + } - case DBALType::JSON_ARRAY: - return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; + $nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property); + $enumType = null; + if (null !== $enumClass = $metadata->getFieldMapping($property)['enumType'] ?? null) { + $enumType = new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass); + } - default: - $builtinType = $this->getPhpType($typeOfField); + switch ($builtinType) { + case Type::BUILTIN_TYPE_OBJECT: + switch ($typeOfField) { + case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: + // no break + case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: + // no break + case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: + // no break + case 'vardatetime': + case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: + return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTime')]; + + case 'date_immutable': + case 'datetime_immutable': + case 'datetimetz_immutable': + case 'time_immutable': + return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateTimeImmutable')]; + + case 'dateinterval': + return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, 'DateInterval')]; + } - return $builtinType ? [new Type($builtinType, $nullable)] : null; + break; + case Type::BUILTIN_TYPE_ARRAY: + switch ($typeOfField) { + case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: + // no break + case 'json_array': + // return null if $enumType is set, because we can't determine if collectionKeyType is string or int + if ($enumType) { + return null; + } + + return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true)]; + + case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + return [new Type(Type::BUILTIN_TYPE_ARRAY, $nullable, null, true, new Type(Type::BUILTIN_TYPE_INT), $enumType ?? new Type(Type::BUILTIN_TYPE_STRING))]; + } + break; + case Type::BUILTIN_TYPE_INT: + case Type::BUILTIN_TYPE_STRING: + if ($enumType) { + return [$enumType]; + } + break; } + + return [new Type($builtinType, $nullable)]; } + + return null; } /** @@ -198,7 +246,7 @@ private function getMetadata(string $class): ?ClassMetadata { try { return $this->entityManager ? $this->entityManager->getClassMetadata($class) : $this->classMetadataFactory->getMetadataFor($class); - } catch (MappingException | OrmMappingException $exception) { + } catch (MappingException|OrmMappingException $exception) { return null; } } @@ -234,29 +282,57 @@ private function isAssociationNullable(array $associationMapping): bool private function getPhpType(string $doctrineType): ?string { switch ($doctrineType) { - case DBALType::SMALLINT: - case DBALType::INTEGER: + case self::$useDeprecatedConstants ? DBALType::SMALLINT : Types::SMALLINT: + // no break + case self::$useDeprecatedConstants ? DBALType::INTEGER : Types::INTEGER: return Type::BUILTIN_TYPE_INT; - case DBALType::FLOAT: + case self::$useDeprecatedConstants ? DBALType::FLOAT : Types::FLOAT: return Type::BUILTIN_TYPE_FLOAT; - case DBALType::BIGINT: - case DBALType::STRING: - case DBALType::TEXT: - case DBALType::GUID: - case DBALType::DECIMAL: + case self::$useDeprecatedConstants ? DBALType::BIGINT : Types::BIGINT: + // no break + case self::$useDeprecatedConstants ? DBALType::STRING : Types::STRING: + // no break + case self::$useDeprecatedConstants ? DBALType::TEXT : Types::TEXT: + // no break + case self::$useDeprecatedConstants ? DBALType::GUID : Types::GUID: + // no break + case self::$useDeprecatedConstants ? DBALType::DECIMAL : Types::DECIMAL: return Type::BUILTIN_TYPE_STRING; - case DBALType::BOOLEAN: + case self::$useDeprecatedConstants ? DBALType::BOOLEAN : Types::BOOLEAN: return Type::BUILTIN_TYPE_BOOL; - case DBALType::BLOB: + case self::$useDeprecatedConstants ? DBALType::BLOB : Types::BLOB: + // no break case 'binary': return Type::BUILTIN_TYPE_RESOURCE; - case DBALType::OBJECT: + case self::$useDeprecatedConstants ? DBALType::OBJECT : Types::OBJECT: + // no break + case self::$useDeprecatedConstants ? DBALType::DATE : Types::DATE_MUTABLE: + // no break + case self::$useDeprecatedConstants ? DBALType::DATETIME : Types::DATETIME_MUTABLE: + // no break + case self::$useDeprecatedConstants ? DBALType::DATETIMETZ : Types::DATETIMETZ_MUTABLE: + // no break + case 'vardatetime': + case self::$useDeprecatedConstants ? DBALType::TIME : Types::TIME_MUTABLE: + // no break + case 'date_immutable': + case 'datetime_immutable': + case 'datetimetz_immutable': + case 'time_immutable': + case 'dateinterval': return Type::BUILTIN_TYPE_OBJECT; + + case self::$useDeprecatedConstants ? DBALType::TARRAY : Types::ARRAY: + // no break + case self::$useDeprecatedConstants ? DBALType::SIMPLE_ARRAY : Types::SIMPLE_ARRAY: + // no break + case 'json_array': + return Type::BUILTIN_TYPE_ARRAY; } return null; diff --git a/src/Symfony/Bridge/Doctrine/README.md b/src/Symfony/Bridge/Doctrine/README.md index 46d897d061e0f..fb7b1cdf745bf 100644 --- a/src/Symfony/Bridge/Doctrine/README.md +++ b/src/Symfony/Bridge/Doctrine/README.md @@ -1,13 +1,13 @@ Doctrine Bridge =============== -Provides integration for [Doctrine](http://www.doctrine-project.org/) with -various Symfony components. +The Doctrine bridge provides integration for +[Doctrine](http://www.doctrine-project.org/) with various Symfony components. Resources --------- - * [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) + * [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/src/Symfony/Bridge/Doctrine/RegistryInterface.php b/src/Symfony/Bridge/Doctrine/RegistryInterface.php index 6928f8afd4f9c..e62b3ba49ca23 100644 --- a/src/Symfony/Bridge/Doctrine/RegistryInterface.php +++ b/src/Symfony/Bridge/Doctrine/RegistryInterface.php @@ -11,15 +11,17 @@ namespace Symfony\Bridge\Doctrine; -use Doctrine\Common\Persistence\ManagerRegistry as ManagerRegistryInterface; use Doctrine\ORM\EntityManager; +use Doctrine\Persistence\ManagerRegistry; /** * References Doctrine connections and entity managers. * + * @deprecated since Symfony 4.4, use Doctrine\Persistence\ManagerRegistry instead + * * @author Fabien Potencier */ -interface RegistryInterface extends ManagerRegistryInterface +interface RegistryInterface extends ManagerRegistry { /** * Gets the default entity manager name. diff --git a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php index 64515fac71840..8f8256f6cb99b 100644 --- a/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/RememberMe/DoctrineTokenProvider.php @@ -12,7 +12,11 @@ namespace Symfony\Bridge\Doctrine\Security\RememberMe; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Types\Type as DoctrineType; +use Doctrine\DBAL\Driver\Result as DriverResult; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Result; +use Doctrine\DBAL\Types\Type; +use Doctrine\DBAL\Types\Types; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentTokenInterface; use Symfony\Component\Security\Core\Authentication\RememberMe\TokenProviderInterface; @@ -40,9 +44,15 @@ class DoctrineTokenProvider implements TokenProviderInterface { private $conn; + private static $useDeprecatedConstants; + public function __construct(Connection $conn) { $this->conn = $conn; + + if (null === self::$useDeprecatedConstants) { + self::$useDeprecatedConstants = !class_exists(Types::class); + } } /** @@ -54,9 +64,9 @@ public function loadTokenBySeries($series) $sql = 'SELECT class, username, value, lastUsed AS last_used' .' FROM rememberme_token WHERE series=:series'; $paramValues = ['series' => $series]; - $paramTypes = ['series' => \PDO::PARAM_STR]; + $paramTypes = ['series' => ParameterType::STRING]; $stmt = $this->conn->executeQuery($sql, $paramValues, $paramTypes); - $row = $stmt->fetch(\PDO::FETCH_ASSOC); + $row = $stmt instanceof Result || $stmt instanceof DriverResult ? $stmt->fetchAssociative() : $stmt->fetch(\PDO::FETCH_ASSOC); if ($row) { return new PersistentToken($row['class'], $row['username'], $series, $row['value'], new \DateTime($row['last_used'])); @@ -72,8 +82,12 @@ public function deleteTokenBySeries($series) { $sql = 'DELETE FROM rememberme_token WHERE series=:series'; $paramValues = ['series' => $series]; - $paramTypes = ['series' => \PDO::PARAM_STR]; - $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + $paramTypes = ['series' => ParameterType::STRING]; + if (method_exists($this->conn, 'executeStatement')) { + $this->conn->executeStatement($sql, $paramValues, $paramTypes); + } else { + $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + } } /** @@ -89,11 +103,15 @@ public function updateToken($series, $tokenValue, \DateTime $lastUsed) 'series' => $series, ]; $paramTypes = [ - 'value' => \PDO::PARAM_STR, - 'lastUsed' => DoctrineType::DATETIME, - 'series' => \PDO::PARAM_STR, + 'value' => ParameterType::STRING, + 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, + 'series' => ParameterType::STRING, ]; - $updated = $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + if (method_exists($this->conn, 'executeStatement')) { + $updated = $this->conn->executeStatement($sql, $paramValues, $paramTypes); + } else { + $updated = $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + } if ($updated < 1) { throw new TokenNotFoundException('No token found.'); } @@ -115,12 +133,16 @@ public function createNewToken(PersistentTokenInterface $token) 'lastUsed' => $token->getLastUsed(), ]; $paramTypes = [ - 'class' => \PDO::PARAM_STR, - 'username' => \PDO::PARAM_STR, - 'series' => \PDO::PARAM_STR, - 'value' => \PDO::PARAM_STR, - 'lastUsed' => DoctrineType::DATETIME, + 'class' => ParameterType::STRING, + 'username' => ParameterType::STRING, + 'series' => ParameterType::STRING, + 'value' => ParameterType::STRING, + 'lastUsed' => self::$useDeprecatedConstants ? Type::DATETIME : Types::DATETIME_MUTABLE, ]; - $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + if (method_exists($this->conn, 'executeStatement')) { + $this->conn->executeStatement($sql, $paramValues, $paramTypes); + } else { + $this->conn->executeUpdate($sql, $paramValues, $paramTypes); + } } } diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index 20f68399571f9..213af6f36a235 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -11,9 +11,13 @@ namespace Symfony\Bridge\Doctrine\Security\User; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; use Symfony\Component\Security\Core\Exception\UnsupportedUserException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -25,7 +29,7 @@ * @author Fabien Potencier * @author Johannes M. Schmitt */ -class EntityUserProvider implements UserProviderInterface +class EntityUserProvider implements UserProviderInterface, PasswordUpgraderInterface { private $registry; private $managerName; @@ -58,7 +62,10 @@ public function loadUserByUsername($username) } if (null === $user) { - throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + $e = new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + $e->setUsername($username); + + throw $e; } return $user; @@ -83,16 +90,15 @@ public function refreshUser(UserInterface $user) // That's the case when the user has been changed by a form with // validation errors. if (!$id = $this->getClassMetadata()->getIdentifierValues($user)) { - throw new \InvalidArgumentException('You cannot refresh a user '. - 'from the EntityUserProvider that does not contain an identifier. '. - 'The user object has to be serialized with its own identifier '. - 'mapped by Doctrine.' - ); + throw new \InvalidArgumentException('You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine.'); } $refreshedUser = $repository->find($id); if (null === $refreshedUser) { - throw new UsernameNotFoundException(sprintf('User with id %s not found', json_encode($id))); + $e = new UsernameNotFoundException('User with id '.json_encode($id).' not found.'); + $e->setUsername(json_encode($id)); + + throw $e; } } @@ -107,22 +113,38 @@ public function supportsClass($class) return $class === $this->getClass() || is_subclass_of($class, $this->getClass()); } - private function getObjectManager() + /** + * {@inheritdoc} + */ + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + $class = $this->getClass(); + if (!$user instanceof $class) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $repository = $this->getRepository(); + if ($repository instanceof PasswordUpgraderInterface) { + $repository->upgradePassword($user, $newEncodedPassword); + } + } + + private function getObjectManager(): ObjectManager { return $this->registry->getManager($this->managerName); } - private function getRepository() + private function getRepository(): ObjectRepository { return $this->getObjectManager()->getRepository($this->classOrAlias); } - private function getClass() + private function getClass(): string { if (null === $this->class) { $class = $this->classOrAlias; - if (false !== strpos($class, ':')) { + if (str_contains($class, ':')) { $class = $this->getClassMetadata()->getName(); } @@ -132,8 +154,11 @@ private function getClass() return $this->class; } - private function getClassMetadata() + private function getClassMetadata(): ClassMetadata { return $this->getObjectManager()->getClassMetadata($this->classOrAlias); } } + +interface_exists(ObjectManager::class); +interface_exists(ObjectRepository::class); diff --git a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php b/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php index 709b66e01bf99..d3d25c17b275d 100644 --- a/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php +++ b/src/Symfony/Bridge/Doctrine/Test/DoctrineTestHelper.php @@ -12,13 +12,12 @@ namespace Symfony\Bridge\Doctrine\Test; use Doctrine\Common\Annotations\AnnotationReader; -use Doctrine\Common\Cache\ArrayCache; -use Doctrine\Common\Persistence\Mapping\Driver\MappingDriverChain; -use Doctrine\Common\Persistence\Mapping\Driver\SymfonyFileLocator; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\Driver\AnnotationDriver; use Doctrine\ORM\Mapping\Driver\XmlDriver; +use Doctrine\Persistence\Mapping\Driver\MappingDriverChain; +use Doctrine\Persistence\Mapping\Driver\SymfonyFileLocator; use PHPUnit\Framework\TestCase; /** @@ -31,8 +30,6 @@ class DoctrineTestHelper /** * Returns an entity manager for testing. * - * @param Configuration|null $config - * * @return EntityManager */ public static function createTestEntityManager(Configuration $config = null) @@ -64,8 +61,6 @@ public static function createTestConfiguration() $config->setProxyDir(sys_get_temp_dir()); $config->setProxyNamespace('SymfonyTests\Doctrine'); $config->setMetadataDriverImpl(new AnnotationDriver(new AnnotationReader())); - $config->setQueryCacheImpl(new ArrayCache()); - $config->setMetadataCacheImpl(new ArrayCache()); return $config; } diff --git a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php b/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php index e7df3702ebf1f..6197c6ae5169c 100644 --- a/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php +++ b/src/Symfony/Bridge/Doctrine/Test/TestRepositoryFactory.php @@ -11,10 +11,10 @@ namespace Symfony\Bridge\Doctrine\Test; -use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Repository\RepositoryFactory; +use Doctrine\Persistence\ObjectRepository; /** * @author Andreas Braun @@ -28,6 +28,8 @@ final class TestRepositoryFactory implements RepositoryFactory /** * {@inheritdoc} + * + * @return ObjectRepository */ public function getRepository(EntityManagerInterface $entityManager, $entityName) { @@ -40,17 +42,14 @@ public function getRepository(EntityManagerInterface $entityManager, $entityName return $this->repositoryList[$repositoryHash] = $this->createRepository($entityManager, $entityName); } - public function setRepository(EntityManagerInterface $entityManager, $entityName, ObjectRepository $repository) + public function setRepository(EntityManagerInterface $entityManager, string $entityName, ObjectRepository $repository) { $repositoryHash = $this->getRepositoryHash($entityManager, $entityName); $this->repositoryList[$repositoryHash] = $repository; } - /** - * @return ObjectRepository - */ - private function createRepository(EntityManagerInterface $entityManager, $entityName) + private function createRepository(EntityManagerInterface $entityManager, string $entityName): ObjectRepository { /* @var $metadata ClassMetadata */ $metadata = $entityManager->getClassMetadata($entityName); @@ -59,7 +58,7 @@ private function createRepository(EntityManagerInterface $entityManager, $entity return new $repositoryClassName($entityManager, $metadata); } - private function getRepositoryHash(EntityManagerInterface $entityManager, $entityName) + private function getRepositoryHash(EntityManagerInterface $entityManager, string $entityName): string { return $entityManager->getClassMetadata($entityName)->getName().spl_object_hash($entityManager); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php index b3fb8bc3ac94e..868fc25cf6151 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ContainerAwareEventManagerTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Doctrine\Tests; +use Doctrine\Common\EventSubscriber; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\ContainerAwareEventManager; use Symfony\Component\DependencyInjection\Container; @@ -20,7 +21,7 @@ class ContainerAwareEventManagerTest extends TestCase private $container; private $evm; - protected function setUp() + protected function setUp(): void { $this->container = new Container(); $this->evm = new ContainerAwareEventManager($this->container); @@ -28,43 +29,131 @@ protected function setUp() public function testDispatchEvent() { - $this->container->set('lazy', $listener1 = new MyListener()); - $this->evm->addEventListener('foo', 'lazy'); + $this->evm = new ContainerAwareEventManager($this->container, ['lazy4']); + + $this->container->set('lazy1', $listener1 = new MyListener()); + $this->evm->addEventListener('foo', 'lazy1'); $this->evm->addEventListener('foo', $listener2 = new MyListener()); + $this->container->set('lazy2', $listener3 = new MyListener()); + $this->evm->addEventListener('bar', 'lazy2'); + $this->evm->addEventListener('bar', $listener4 = new MyListener()); + $this->container->set('lazy3', $listener5 = new MyListener()); + $this->evm->addEventListener('foo', $listener5 = new MyListener()); + $this->evm->addEventListener('bar', $listener5); + $this->container->set('lazy4', $subscriber1 = new MySubscriber(['foo'])); + $this->evm->addEventSubscriber($subscriber2 = new MySubscriber(['bar'])); - $this->evm->dispatchEvent('foo'); + $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); - $this->assertTrue($listener1->called); - $this->assertTrue($listener2->called); + $this->evm->dispatchEvent('foo'); + $this->evm->dispatchEvent('bar'); + + $this->assertSame(1, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); + + $this->assertSame(0, $listener1->calledByInvokeCount); + $this->assertSame(1, $listener1->calledByEventNameCount); + $this->assertSame(0, $listener2->calledByInvokeCount); + $this->assertSame(1, $listener2->calledByEventNameCount); + $this->assertSame(1, $listener3->calledByInvokeCount); + $this->assertSame(0, $listener3->calledByEventNameCount); + $this->assertSame(1, $listener4->calledByInvokeCount); + $this->assertSame(0, $listener4->calledByEventNameCount); + $this->assertSame(1, $listener5->calledByInvokeCount); + $this->assertSame(1, $listener5->calledByEventNameCount); + $this->assertSame(0, $subscriber1->calledByInvokeCount); + $this->assertSame(1, $subscriber1->calledByEventNameCount); + $this->assertSame(1, $subscriber2->calledByInvokeCount); + $this->assertSame(0, $subscriber2->calledByEventNameCount); } - public function testAddEventListenerAfterDispatchEvent() + public function testAddEventListenerAndSubscriberAfterDispatchEvent() { + $this->evm = new ContainerAwareEventManager($this->container, ['lazy7']); + $this->container->set('lazy1', $listener1 = new MyListener()); $this->evm->addEventListener('foo', 'lazy1'); $this->evm->addEventListener('foo', $listener2 = new MyListener()); - - $this->evm->dispatchEvent('foo'); - $this->container->set('lazy2', $listener3 = new MyListener()); - $this->evm->addEventListener('foo', 'lazy2'); - $this->evm->addEventListener('foo', $listener4 = new MyListener()); + $this->evm->addEventListener('bar', 'lazy2'); + $this->evm->addEventListener('bar', $listener4 = new MyListener()); + $this->container->set('lazy3', $listener5 = new MyListener()); + $this->evm->addEventListener('foo', $listener5 = new MyListener()); + $this->evm->addEventListener('bar', $listener5); + $this->container->set('lazy7', $subscriber1 = new MySubscriber(['foo'])); + $this->evm->addEventSubscriber($subscriber2 = new MySubscriber(['bar'])); + + $this->assertSame(0, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); $this->evm->dispatchEvent('foo'); + $this->evm->dispatchEvent('bar'); + + $this->assertSame(1, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); + + $this->container->set('lazy4', $listener6 = new MyListener()); + $this->evm->addEventListener('foo', 'lazy4'); + $this->evm->addEventListener('foo', $listener7 = new MyListener()); + $this->container->set('lazy5', $listener8 = new MyListener()); + $this->evm->addEventListener('bar', 'lazy5'); + $this->evm->addEventListener('bar', $listener9 = new MyListener()); + $this->container->set('lazy6', $listener10 = new MyListener()); + $this->evm->addEventListener('foo', $listener10 = new MyListener()); + $this->evm->addEventListener('bar', $listener10); + $this->evm->addEventSubscriber($subscriber3 = new MySubscriber(['bar'])); + + $this->assertSame(1, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber3->calledSubscribedEventsCount); - $this->assertTrue($listener1->called); - $this->assertTrue($listener2->called); - $this->assertTrue($listener3->called); - $this->assertTrue($listener4->called); + $this->evm->dispatchEvent('foo'); + $this->evm->dispatchEvent('bar'); + + $this->assertSame(1, $subscriber1->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber2->calledSubscribedEventsCount); + $this->assertSame(1, $subscriber3->calledSubscribedEventsCount); + + $this->assertSame(0, $listener1->calledByInvokeCount); + $this->assertSame(2, $listener1->calledByEventNameCount); + $this->assertSame(0, $listener2->calledByInvokeCount); + $this->assertSame(2, $listener2->calledByEventNameCount); + $this->assertSame(2, $listener3->calledByInvokeCount); + $this->assertSame(0, $listener3->calledByEventNameCount); + $this->assertSame(2, $listener4->calledByInvokeCount); + $this->assertSame(0, $listener4->calledByEventNameCount); + $this->assertSame(2, $listener5->calledByInvokeCount); + $this->assertSame(2, $listener5->calledByEventNameCount); + $this->assertSame(0, $subscriber1->calledByInvokeCount); + $this->assertSame(2, $subscriber1->calledByEventNameCount); + $this->assertSame(2, $subscriber2->calledByInvokeCount); + $this->assertSame(0, $subscriber2->calledByEventNameCount); + + $this->assertSame(0, $listener6->calledByInvokeCount); + $this->assertSame(1, $listener6->calledByEventNameCount); + $this->assertSame(0, $listener7->calledByInvokeCount); + $this->assertSame(1, $listener7->calledByEventNameCount); + $this->assertSame(1, $listener8->calledByInvokeCount); + $this->assertSame(0, $listener8->calledByEventNameCount); + $this->assertSame(1, $listener9->calledByInvokeCount); + $this->assertSame(0, $listener9->calledByEventNameCount); + $this->assertSame(1, $listener10->calledByInvokeCount); + $this->assertSame(1, $listener10->calledByEventNameCount); + $this->assertSame(1, $subscriber3->calledByInvokeCount); + $this->assertSame(0, $subscriber3->calledByEventNameCount); } public function testGetListenersForEvent() { + $this->evm = new ContainerAwareEventManager($this->container, ['lazy2']); + $this->container->set('lazy', $listener1 = new MyListener()); + $this->container->set('lazy2', $subscriber1 = new MySubscriber(['foo'])); $this->evm->addEventListener('foo', 'lazy'); $this->evm->addEventListener('foo', $listener2 = new MyListener()); - $this->assertSame([$listener1, $listener2], array_values($this->evm->getListeners('foo'))); + $this->assertSame([$subscriber1, $listener1, $listener2], array_values($this->evm->getListeners('foo'))); } public function testGetListeners() @@ -76,6 +165,15 @@ public function testGetListeners() $this->assertSame([$listener1, $listener2], array_values($this->evm->getListeners()['foo'])); } + public function testGetAllListeners() + { + $this->container->set('lazy', $listener1 = new MyListener()); + $this->evm->addEventListener('foo', 'lazy'); + $this->evm->addEventListener('foo', $listener2 = new MyListener()); + + $this->assertSame([$listener1, $listener2], array_values($this->evm->getAllListeners()['foo'])); + } + public function testRemoveEventListener() { $this->container->set('lazy', $listener1 = new MyListener()); @@ -107,10 +205,34 @@ public function testRemoveEventListenerAfterDispatchEvent() class MyListener { - public $called = false; + public $calledByInvokeCount = 0; + public $calledByEventNameCount = 0; + + public function __invoke(): void + { + ++$this->calledByInvokeCount; + } public function foo() { - $this->called = true; + ++$this->calledByEventNameCount; + } +} + +class MySubscriber extends MyListener implements EventSubscriber +{ + public $calledSubscribedEventsCount = 0; + private $listenedEvents; + + public function __construct(array $listenedEvents) + { + $this->listenedEvents = $listenedEvents; + } + + public function getSubscribedEvents(): array + { + ++$this->calledSubscribedEventsCount; + + return $this->listenedEvents; } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php index 6a33f0680a7a3..35fc48ff1536f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataCollector/DoctrineDataCollectorTest.php @@ -11,12 +11,19 @@ namespace Symfony\Bridge\Doctrine\Tests\DataCollector; -use Doctrine\DBAL\Platforms\MySqlPlatform; -use Doctrine\DBAL\Version; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Logging\DebugStack; +use Doctrine\DBAL\Platforms\MySQLPlatform; +use Doctrine\Persistence\ManagerRegistry; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\DataCollector\DoctrineDataCollector; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Dumper\CliDumper; + +// Doctrine DBAL 2 compatibility +class_exists(\Doctrine\DBAL\Platforms\MySqlPlatform::class); class DoctrineDataCollectorTest extends TestCase { @@ -73,7 +80,7 @@ public function testCollectTime() /** * @dataProvider paramProvider */ - public function testCollectQueries($param, $types, $expected, $explainable) + public function testCollectQueries($param, $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -82,8 +89,21 @@ public function testCollectQueries($param, $types, $expected, $explainable) $c->collect(new Request(), new Response()); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } elseif (\is_string($expected)) { + $this->assertStringMatchesFormat($expected, $collectedParam); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } public function testCollectQueryWithNoParams() @@ -96,10 +116,14 @@ public function testCollectQueryWithNoParams() $c->collect(new Request(), new Response()); $collectedQueries = $c->getQueries(); - $this->assertEquals([], $collectedQueries['default'][0]['params']); + $this->assertInstanceOf(Data::class, $collectedQueries['default'][0]['params']); + $this->assertEquals([], $collectedQueries['default'][0]['params']->getValue()); $this->assertTrue($collectedQueries['default'][0]['explainable']); - $this->assertEquals([], $collectedQueries['default'][1]['params']); + $this->assertTrue($collectedQueries['default'][0]['runnable']); + $this->assertInstanceOf(Data::class, $collectedQueries['default'][1]['params']); + $this->assertEquals([], $collectedQueries['default'][1]['params']->getValue()); $this->assertTrue($collectedQueries['default'][1]['explainable']); + $this->assertTrue($collectedQueries['default'][1]['runnable']); } public function testCollectQueryWithNoTypes() @@ -131,7 +155,7 @@ public function testReset() /** * @dataProvider paramProvider */ - public function testSerialization($param, $types, $expected, $explainable) + public function testSerialization($param, array $types, $expected, $explainable, bool $runnable = true) { $queries = [ ['sql' => 'SELECT * FROM table1 WHERE field1 = ?1', 'params' => [$param], 'types' => $types, 'executionMS' => 1], @@ -141,46 +165,81 @@ public function testSerialization($param, $types, $expected, $explainable) $c = unserialize(serialize($c)); $collectedQueries = $c->getQueries(); - $this->assertEquals($expected, $collectedQueries['default'][0]['params'][0]); + + $collectedParam = $collectedQueries['default'][0]['params'][0]; + if ($collectedParam instanceof Data) { + $dumper = new CliDumper($out = fopen('php://memory', 'r+')); + $dumper->setColors(false); + $collectedParam->dump($dumper); + $this->assertStringMatchesFormat($expected, print_r(stream_get_contents($out, -1, 0), true)); + } elseif (\is_string($expected)) { + $this->assertStringMatchesFormat($expected, $collectedParam); + } else { + $this->assertEquals($expected, $collectedParam); + } + $this->assertEquals($explainable, $collectedQueries['default'][0]['explainable']); + $this->assertSame($runnable, $collectedQueries['default'][0]['runnable']); } - public function paramProvider() + public function paramProvider(): array { - $tests = [ + return [ ['some value', [], 'some value', true], [1, [], 1, true], [true, [], true, true], [null, [], null, true], [new \DateTime('2011-09-11'), ['date'], '2011-09-11', true], - [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false], - [new \stdClass(), [], '/* Object(stdClass) */', false], + [fopen(__FILE__, 'r'), [], '/* Resource(stream) */', false, false], + [ + new \stdClass(), + [], + <<=')) { - $tests[] = ['this is not a date', ['date'], 'this is not a date', false]; - $tests[] = [new \stdClass(), ['date'], '/* Object(stdClass) */', false]; - } - - return $tests; } - private function createCollector($queries) + private function createCollector(array $queries): DoctrineDataCollector { - $connection = $this->getMockBuilder('Doctrine\DBAL\Connection') + $connection = $this->getMockBuilder(Connection::class) ->disableOriginalConstructor() ->getMock(); $connection->expects($this->any()) ->method('getDatabasePlatform') ->willReturn(new MySqlPlatform()); - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->createMock(ManagerRegistry::class); $registry ->expects($this->any()) ->method('getConnectionNames') @@ -193,7 +252,7 @@ private function createCollector($queries) ->method('getConnection') ->willReturn($connection); - $logger = $this->getMockBuilder('Doctrine\DBAL\Logging\DebugStack')->getMock(); + $logger = $this->createMock(DebugStack::class); $logger->queries = $queries; $collector = new DoctrineDataCollector($registry); @@ -205,7 +264,7 @@ private function createCollector($queries) class StringRepresentableClass { - public function __toString() + public function __toString(): string { return 'string representation'; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php index 422c459b46afa..dd7a63fea4683 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DataFixtures/ContainerAwareLoaderTest.php @@ -14,12 +14,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader; use Symfony\Bridge\Doctrine\Tests\Fixtures\ContainerAwareFixture; +use Symfony\Component\DependencyInjection\ContainerInterface; class ContainerAwareLoaderTest extends TestCase { public function testShouldSetContainerOnContainerAwareFixture() { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); $loader = new ContainerAwareLoader($container); $fixture = new ContainerAwareFixture(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php index ca75437b769f4..28b983324e55d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterEventListenersAndSubscribersPassTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\DependencyInjection\CompilerPass; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\ContainerAwareEventManager; use Symfony\Bridge\Doctrine\DependencyInjection\CompilerPass\RegisterEventListenersAndSubscribersPass; use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -21,11 +22,9 @@ class RegisterEventListenersAndSubscribersPassTest extends TestCase { - /** - * @expectedException \InvalidArgumentException - */ public function testExceptionOnAbstractTaggedSubscriber() { + $this->expectException(\InvalidArgumentException::class); $container = $this->createBuilder(); $abstractDefinition = new Definition('stdClass'); @@ -37,11 +36,9 @@ public function testExceptionOnAbstractTaggedSubscriber() $this->process($container); } - /** - * @expectedException \InvalidArgumentException - */ public function testExceptionOnAbstractTaggedListener() { + $this->expectException(\InvalidArgumentException::class); $container = $this->createBuilder(); $abstractDefinition = new Definition('stdClass'); @@ -213,20 +210,46 @@ public function testProcessEventSubscribersWithMultipleConnections() $this->process($container); + $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); + + // first connection + $this->assertEquals( + [ + 'a', + 'b', + ], + $eventManagerDef->getArgument(1) + ); + + $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); + $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); + $this->assertEquals( + [ + 'a' => new ServiceClosureArgument(new Reference('a')), + 'b' => new ServiceClosureArgument(new Reference('b')), + ], + $serviceLocatorDef->getArgument(0) + ); + + $eventManagerDef = $container->getDefinition('doctrine.dbal.second_connection.event_manager'); + + // second connection $this->assertEquals( [ - ['addEventSubscriber', [new Reference('a')]], - ['addEventSubscriber', [new Reference('b')]], + 'a', + 'c', ], - $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls() + $eventManagerDef->getArgument(1) ); + $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); + $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); $this->assertEquals( [ - ['addEventSubscriber', [new Reference('a')]], - ['addEventSubscriber', [new Reference('c')]], + 'a' => new ServiceClosureArgument(new Reference('a')), + 'c' => new ServiceClosureArgument(new Reference('c')), ], - $container->getDefinition('doctrine.dbal.second_connection.event_manager')->getMethodCalls() + $serviceLocatorDef->getArgument(0) ); } @@ -265,15 +288,30 @@ public function testProcessEventSubscribersWithPriorities() $this->process($container); + $eventManagerDef = $container->getDefinition('doctrine.dbal.default_connection.event_manager'); + $this->assertEquals( [ - ['addEventSubscriber', [new Reference('c')]], - ['addEventSubscriber', [new Reference('d')]], - ['addEventSubscriber', [new Reference('e')]], - ['addEventSubscriber', [new Reference('b')]], - ['addEventSubscriber', [new Reference('a')]], + 'c', + 'd', + 'e', + 'b', + 'a', ], - $container->getDefinition('doctrine.dbal.default_connection.event_manager')->getMethodCalls() + $eventManagerDef->getArgument(1) + ); + + $serviceLocatorDef = $container->getDefinition((string) $eventManagerDef->getArgument(0)); + $this->assertSame(ServiceLocator::class, $serviceLocatorDef->getClass()); + $this->assertEquals( + [ + 'a' => new ServiceClosureArgument(new Reference('a')), + 'b' => new ServiceClosureArgument(new Reference('b')), + 'c' => new ServiceClosureArgument(new Reference('c')), + 'd' => new ServiceClosureArgument(new Reference('d')), + 'e' => new ServiceClosureArgument(new Reference('e')), + ], + $serviceLocatorDef->getArgument(0) ); } @@ -300,12 +338,12 @@ private function createBuilder($multipleConnections = false) $connections = ['default' => 'doctrine.dbal.default_connection']; - $container->register('doctrine.dbal.default_connection.event_manager', 'stdClass') + $container->register('doctrine.dbal.default_connection.event_manager', ContainerAwareEventManager::class) ->addArgument(new Reference('service_container')); $container->register('doctrine.dbal.default_connection', 'stdClass'); if ($multipleConnections) { - $container->register('doctrine.dbal.second_connection.event_manager', 'stdClass') + $container->register('doctrine.dbal.second_connection.event_manager', ContainerAwareEventManager::class) ->addArgument(new Reference('service_container')); $container->register('doctrine.dbal.second_connection', 'stdClass'); $connections['second'] = 'doctrine.dbal.second_connection'; diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterMappingsPassTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterMappingsPassTest.php index 0bb2642a7696e..fecc532a0b609 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterMappingsPassTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/CompilerPass/RegisterMappingsPassTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\Doctrine\Tests\DependencyInjection\CompilerPass; use PHPUnit\Framework\TestCase; @@ -9,12 +18,10 @@ class RegisterMappingsPassTest extends TestCase { - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessageould Could not find the manager name parameter in the container. Tried the following parameter names: "manager.param.one", "manager.param.two" - */ public function testNoDriverParmeterException() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Could not find the manager name parameter in the container. Tried the following parameter names: "manager.param.one", "manager.param.two"'); $container = $this->createBuilder(); $this->process($container, [ 'manager.param.one', diff --git a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php index 7e1cef511f6a9..4fb8a0a4437d6 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/DependencyInjection/DoctrineExtensionTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\DependencyInjection; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; @@ -22,16 +23,16 @@ class DoctrineExtensionTest extends TestCase { /** - * @var \Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension + * @var AbstractDoctrineExtension */ private $extension; - protected function setUp() + protected function setUp(): void { parent::setUp(); $this->extension = $this - ->getMockBuilder('Symfony\Bridge\Doctrine\DependencyInjection\AbstractDoctrineExtension') + ->getMockBuilder(AbstractDoctrineExtension::class) ->setMethods([ 'getMappingResourceConfigDirectory', 'getObjectManagerElementName', @@ -49,11 +50,9 @@ protected function setUp() }); } - /** - * @expectedException \LogicException - */ public function testFixManagersAutoMappingsWithTwoAutomappings() { + $this->expectException(\LogicException::class); $emConfigs = [ 'em1' => [ 'auto_mapping' => true, @@ -234,12 +233,10 @@ public function testServiceCacheDriver() $this->assertTrue($container->hasAlias('doctrine.orm.default_metadata_cache')); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage "unrecognized_type" is an unrecognized Doctrine cache driver. - */ public function testUnrecognizedCacheDriverException() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('"unrecognized_type" is an unrecognized Doctrine cache driver.'); $cacheName = 'metadata_cache'; $container = $this->createContainer(); $objectManager = [ @@ -261,10 +258,7 @@ protected function invokeLoadCacheDriver(array $objectManager, ContainerBuilder $method->invokeArgs($this->extension, [$objectManager, $container, $cacheName]); } - /** - * @return \Symfony\Component\DependencyInjection\ContainerBuilder - */ - protected function createContainer(array $data = []) + protected function createContainer(array $data = []): ContainerBuilder { return new ContainerBuilder(new ParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php index 50b5845581ce4..aa24cd68943dd 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/BaseUser.php @@ -24,9 +24,6 @@ class BaseUser /** * BaseUser constructor. - * - * @param int $id - * @param string $username */ public function __construct(int $id, string $username) { @@ -34,18 +31,12 @@ public function __construct(int $id, string $username) $this->username = $username; } - /** - * @return int - */ - public function getId() + public function getId(): int { return $this->id; } - /** - * @return string - */ - public function getUsername() + public function getUsername(): string { return $this->username; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeIntIdEntity.php index 8a9b00ddc73e7..7c64cc20ad7e1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeIntIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeIntIdEntity.php @@ -34,7 +34,7 @@ public function __construct($id1, $id2, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeObjectNoToStringIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeObjectNoToStringIdEntity.php index ac97367094bd5..82811b89ed8c0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeObjectNoToStringIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeObjectNoToStringIdEntity.php @@ -35,18 +35,12 @@ public function __construct(SingleIntIdNoToStringEntity $objectOne, SingleIntIdN $this->objectTwo = $objectTwo; } - /** - 10000 * @return SingleIntIdNoToStringEntity - */ - public function getObjectOne() + public function getObjectOne(): SingleIntIdNoToStringEntity { return $this->objectOne; } - /** - * @return SingleIntIdNoToStringEntity - */ - public function getObjectTwo() + public function getObjectTwo(): SingleIntIdNoToStringEntity { return $this->objectTwo; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeStringIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeStringIdEntity.php index 0755a89e6a923..d6e8d2cd2aafa 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeStringIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/CompositeStringIdEntity.php @@ -34,7 +34,7 @@ public function __construct($id1, $id2, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ContainerAwareFixture.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ContainerAwareFixture.php index 6c3f880eaacf9..6655033ab4999 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ContainerAwareFixture.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/ContainerAwareFixture.php @@ -12,7 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Fixtures; use Doctrine\Common\DataFixtures\FixtureInterface; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectManager; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php index 9a2111f2b92df..06f8674e56d66 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEntity.php @@ -69,6 +69,12 @@ class DoctrineLoaderEntity extends DoctrineLoaderParentEntity /** @ORM\Column(type="simple_array", length=100) */ public $simpleArrayField = []; + /** + * @ORM\Column(length=10) + * @Assert\DisableAutoMapping + */ + public $noAutoMapping; + public static function loadValidatorMetadata(ClassMetadata $metadata): void { $allowEmptyString = property_exists(Assert\Length::class, 'allowEmptyString') ? ['allowEmptyString' => true] : []; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEnum.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEnum.php new file mode 100644 index 0000000000000..8ac883e89c4a2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderEnum.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\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping as ORM; + +/** + * @ORM\Entity + */ +class DoctrineLoaderEnum +{ + /** + * @ORM\Id + * @ORM\Column + */ + public $id; + + /** + * @ORM\Column(type="string", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString", length=1) + */ + public $enumString; + + /** + * @ORM\Column(type="integer", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt") + */ + public $enumInt; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderNoAutoMappingEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderNoAutoMappingEntity.php new file mode 100644 index 0000000000000..0914411431201 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/DoctrineLoaderNoAutoMappingEntity.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\Bridge\Doctrine\Tests\Fixtures; + +use Doctrine\ORM\Mapping as ORM; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * @ORM\Entity + * @Assert\DisableAutoMapping + * + * @author KΓ©vin Dunglas + */ +class DoctrineLoaderNoAutoMappingEntity +{ + /** + * @ORM\Id + * @ORM\Column + */ + public $id; + + /** + * @ORM\Column(length=20, unique=true) + */ + public $maxLength; + + /** + * @Assert\EnableAutoMapping + * @ORM\Column(length=20) + */ + public $autoMappingExplicitlyEnabled; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Person.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Person.php index 6e383394bee47..b90a54ac02c61 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Person.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Person.php @@ -38,7 +38,7 @@ public function __construct($id, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return (string) $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php index 5cd6d407962aa..bed8bb9a51d01 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleAssociationToIntIdEntity.php @@ -31,7 +31,7 @@ public function __construct(SingleIntIdNoToStringEntity $entity, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return (string) $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php index ff29145e3353f..612566b45d94b 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleIntIdEntity.php @@ -33,7 +33,7 @@ public function __construct($id, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return (string) $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.php index e457f69dd091b..128801a02c922 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringCastableIdEntity.php @@ -35,7 +35,7 @@ public function __construct($id, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return (string) $this->name; } @@ -50,7 +50,7 @@ public function __construct($id) $this->id = $id; } - public function __toString() + public function __toString(): string { return (string) $this->id; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringIdEntity.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringIdEntity.php index 3e25e2aea52bd..83f7a9f9ab39d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringIdEntity.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/SingleStringIdEntity.php @@ -30,7 +30,7 @@ public function __construct($id, $name) $this->name = $name; } - public function __toString() + public function __toString(): string { return $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapper.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapper.php index d46798aa84bb4..941ab3ed48ee8 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapper.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapper.php @@ -20,10 +20,7 @@ public function __construct(string $string = null) $this->string = $string; } - /** - * @return string - */ - public function getString() + public function getString(): string { return $this->string; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php index 0af4271ba73fa..d01148f3b018c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/Type/StringWrapperType.php @@ -18,6 +18,8 @@ class StringWrapperType extends StringType { /** * {@inheritdoc} + * + * @return mixed */ public function convertToDatabaseValue($value, AbstractPlatform $platform) { @@ -26,6 +28,8 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) /** * {@inheritdoc} + * + * @return mixed */ public function convertToPHPValue($value, AbstractPlatform $platform) { @@ -35,7 +39,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform) /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return 'string_wrapper'; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php index c2ad425b61eac..c5cbc662fc1d1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/User.php @@ -35,19 +35,19 @@ public function __construct($id1, $id2, $name) $this->name = $name; } - public function getRoles() + public function getRoles(): array { } - public function getPassword() + public function getPassword(): ?string { } - public function getSalt() + public function getSalt(): ?string { } - public function getUsername() + public function getUsername(): string { return $this->name; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php index 5a5fba5afaf57..535795f061060 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php @@ -11,9 +11,10 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\ChoiceList; -use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; @@ -28,17 +29,17 @@ class DoctrineChoiceLoaderTest extends TestCase { /** - * @var ChoiceListFactoryInterface|\PHPUnit_Framework_MockObject_MockObject + * @var MockObject&ChoiceListFactoryInterface */ private $factory; /** - * @var ObjectManager|\PHPUnit_Framework_MockObject_MockObject + * @var MockObject&ObjectManager */ private $om; /** - * @var ObjectRepository|\PHPUnit_Framework_MockObject_MockObject + * @var MockObject&ObjectRepository */ private $repository; @@ -48,12 +49,12 @@ class DoctrineChoiceLoaderTest extends TestCase private $class; /** - * @var IdReader|\PHPUnit_Framework_MockObject_MockObject + * @var MockObject&IdReader */ private $idReader; /** - * @var EntityLoaderInterface|\PHPUnit_Framework_MockObject_MockObject + * @var MockObject&EntityLoaderInterface */ private $objectLoader; @@ -72,21 +73,19 @@ class DoctrineChoiceLoaderTest extends TestCase */ private $obj3; - protected function setUp() + protected function setUp(): void { - $this->factory = $this->getMockBuilder('Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface')->getMock(); - $this->om = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); - $this->repository = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectRepository')->getMock(); + $this->factory = $this->createMock(ChoiceListFactoryInterface::class); + $this->om = $this->createMock(ObjectManager::class); + $this->repository = $this->createMock(ObjectRepository::class); $this->class = 'stdClass'; - $this->idReader = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader') - ->disableOriginalConstructor() - ->getMock(); + $this->idReader = $this->createMock(IdReader::class); $this->idReader->expects($this->any()) ->method('isSingleId') ->willReturn(true) ; - $this->objectLoader = $this->getMockBuilder('Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface')->getMock(); + $this->objectLoader = $this->createMock(EntityLoaderInterface::class); $this->obj1 = (object) ['name' => 'A']; $this->obj2 = (object) ['name' => 'B']; $this->obj3 = (object) ['name' => 'C']; @@ -147,8 +146,7 @@ public function testLoadChoiceListUsesObjectLoaderIfAvailable() $this->assertEquals($choiceList, $loaded = $loader->loadChoiceList()); // no further loads on subsequent calls - - $this->assertSame($loaded, $loader->loadChoiceList()); + $this->assertEquals($loaded, $loader->loadChoiceList()); } public function testLoadValuesForChoices() @@ -317,8 +315,8 @@ public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() $this->assertSame( [4 => $this->obj3, 7 => $this->obj2], - $loader->loadChoicesForValues([4 => '3', 7 => '2'] - )); + $loader->loadChoicesForValues([4 => '3', 7 => '2']) + ); } public function testLoadChoicesForValuesLoadsAllIfSingleIntIdAndValueGiven() diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php index 3abdb3578aaf9..8bb0f256977a5 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/ORMQueryBuilderLoaderTest.php @@ -33,16 +33,20 @@ protected function checkIdentifierType($classname, $expectedType) { $em = DoctrineTestHelper::createTestEntityManager(); - $query = $this->getMockBuilder('QueryMock') + $query = $this->getMockBuilder(\QueryMock::class) ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) ->getMock(); + $query + ->method('getResult') + ->willReturn([]); + $query->expects($this->once()) ->method('setParameter') ->with('ORMQueryBuilderLoader_getEntitiesByIds_id', [1, 2], $expectedType) ->willReturn($query); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) ->setConstructorArgs([$em]) ->setMethods(['getQuery']) ->getMock(); @@ -62,16 +66,20 @@ public function testFilterNonIntegerValues() { $em = DoctrineTestHelper::createTestEntityManager(); - $query = $this->getMockBuilder('QueryMock') + $query = $this->getMockBuilder(\QueryMock::class) ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) ->getMock(); + $query + ->method('getResult') + ->willReturn([]); + $query->expects($this->once()) ->method('setParameter') ->with('ORMQueryBuilderLoader_getEntitiesByIds_id', [1, 2, 3, '9223372036854775808'], Connection::PARAM_INT_ARRAY) ->willReturn($query); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) ->setConstructorArgs([$em]) ->setMethods(['getQuery']) ->getMock(); @@ -94,16 +102,20 @@ public function testFilterEmptyUuids($entityClass) { $em = DoctrineTestHelper::createTestEntityManager(); - $query = $this->getMockBuilder('QueryMock') + $query = $this->getMockBuilder(\QueryMock::class) ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) ->getMock(); + $query + ->method('getResult') + ->willReturn([]); + $query->expects($this->once()) ->method('setParameter') ->with('ORMQueryBuilderLoader_getEntitiesByIds_id', ['71c5fd46-3f16-4abb-bad7-90ac1e654a2d', 'b98e8e11-2897-44df-ad24-d2627eb7f499'], Connection::PARAM_STR_ARRAY) ->willReturn($query); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) ->setConstructorArgs([$em]) ->setMethods(['getQuery']) ->getMock(); @@ -129,16 +141,20 @@ public function testEmbeddedIdentifierName() $em = DoctrineTestHelper::createTestEntityManager(); - $query = $this->getMockBuilder('QueryMock') + $query = $this->getMockBuilder(\QueryMock::class) ->setMethods(['setParameter', 'getResult', 'getSql', '_doExecute']) ->getMock(); + $query + ->method('getResult') + ->willReturn([]); + $query->expects($this->once()) ->method('setParameter') ->with('ORMQueryBuilderLoader_getEntitiesByIds_id_value', [1, 2, 3], Connection::PARAM_INT_ARRAY) ->willReturn($query); - $qb = $this->getMockBuilder('Doctrine\ORM\QueryBuilder') + $qb = $this->getMockBuilder(\Doctrine\ORM\QueryBuilder::class) ->setConstructorArgs([$em]) ->setMethods(['getQuery']) ->getMock(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php index e6e85f4d3f7df..48470c607db8c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/DataTransformer/CollectionToArrayTransformerTest.php @@ -14,6 +14,7 @@ use Doctrine\Common\Collections\ArrayCollection; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; +use Symfony\Component\Form\Exception\TransformationFailedException; /** * @author Bernhard Schussek @@ -25,7 +26,7 @@ class CollectionToArrayTransformerTest extends TestCase */ private $transformer; - protected function setUp() + protected function setUp(): void { $this->transformer = new CollectionToArrayTransformer(); } @@ -62,11 +63,9 @@ public function testTransformNull() $this->assertSame([], $this->transformer->transform(null)); } - /** - * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException - */ public function testTransformExpectsArrayOrCollection() { + $this->expectException(TransformationFailedException::class); $this->transformer->transform('Foo'); } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php index d2e101b4cdc58..e003a20ee6b56 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/DoctrineOrmTypeGuesserTest.php @@ -11,7 +11,9 @@ namespace Symfony\Bridge\Doctrine\Tests\Form; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\ObjectManager; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Component\Form\Guess\Guess; @@ -32,21 +34,21 @@ public function requiredProvider() $return = []; // Simple field, not nullable - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->fieldMappings['field'] = true; $classMetadata->expects($this->once())->method('isNullable')->with('field')->willReturn(false); $return[] = [$classMetadata, new ValueGuess(true, Guess::HIGH_CONFIDENCE)]; // Simple field, nullable - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->fieldMappings['field'] = true; $classMetadata->expects($this->once())->method('isNullable')->with('field')->willReturn(true); $return[] = [$classMetadata, new ValueGuess(false, Guess::MEDIUM_CONFIDENCE)]; // One-to-one, nullable (by default) - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->expects($this->once())->method('isAssociationWithSingleJoinColumn')->with('field')->willReturn(true); $mapping = ['joinColumns' => [[]]]; @@ -55,7 +57,7 @@ public function requiredProvider() $return[] = [$classMetadata, new ValueGuess(false, Guess::HIGH_CONFIDENCE)]; // One-to-one, nullable (explicit) - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->expects($this->once())->method('isAssociationWithSingleJoinColumn')->with('field')->willReturn(true); $mapping = ['joinColumns' => [['nullable' => true]]]; @@ -64,7 +66,7 @@ public function requiredProvider() $return[] = [$classMetadata, new ValueGuess(false, Guess::HIGH_CONFIDENCE)]; // One-to-one, not nullable - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->expects($this->once())->method('isAssociationWithSingleJoinColumn')->with('field')->willReturn(true); $mapping = ['joinColumns' => [['nullable' => false]]]; @@ -73,7 +75,7 @@ public function requiredProvider() $return[] = [$classMetadata, new ValueGuess(true, Guess::HIGH_CONFIDENCE)]; // One-to-many, no clue - $classMetadata = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata')->disableOriginalConstructor()->getMock(); + $classMetadata = $this->createMock(ClassMetadata::class); $classMetadata->expects($this->once())->method('isAssociationWithSingleJoinColumn')->with('field')->willReturn(false); $return[] = [$classMetadata, null]; @@ -83,10 +85,10 @@ public function requiredProvider() private function getGuesser(ClassMetadata $classMetadata) { - $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager')->getMock(); + $em = $this->createMock(ObjectManager::class); $em->expects($this->once())->method('getClassMetaData')->with('TestEntity')->willReturn($classMetadata); - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->createMock(ManagerRegistry::class); $registry->expects($this->once())->method('getManagers')->willReturn([$em]); return new DoctrineOrmTypeGuesser($registry); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/EventListener/MergeDoctrineCollectionListenerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/EventListener/MergeDoctrineCollectionListenerTest.php index 757cdc3934c99..bbc69237ff36f 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/EventListener/MergeDoctrineCollectionListenerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/EventListener/MergeDoctrineCollectionListenerTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\FormBuilder; use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; +use Symfony\Component\Form\FormFactoryInterface; class MergeDoctrineCollectionListenerTest extends TestCase { @@ -28,16 +29,16 @@ class MergeDoctrineCollectionListenerTest extends TestCase private $factory; private $form; - protected function setUp() + protected function setUp(): void { $this->collection = new ArrayCollection(['test']); $this->dispatcher = new EventDispatcher(); - $this->factory = $this->getMockBuilder('Symfony\Component\Form\FormFactoryInterface')->getMock(); + $this->factory = $this->createMock(FormFactoryInterface::class); $this->form = $this->getBuilder() ->getForm(); } - protected function tearDown() + protected function tearDown(): void { $this->collection = null; $this->dispatcher = null; diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php index 5dc184fb91009..4b0a93884be3c 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypePerformanceTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; @@ -23,18 +24,16 @@ */ class EntityTypePerformanceTest extends FormPerformanceTestCase { - const ENTITY_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; + private const ENTITY_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; /** * @var \Doctrine\ORM\EntityManager */ private $em; - protected static $supportedFeatureSetVersion = 304; - protected function getExtensions() { - $manager = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $manager = $this->createMock(ManagerRegistry::class); $manager->expects($this->any()) ->method('getManager') @@ -50,7 +49,7 @@ protected function getExtensions() ]; } - protected function setUp() + protected function setUp(): void { $this->em = DoctrineTestHelper::createTestEntityManager(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index 0a9bf739fc224..f593618f9c8ff 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -12,10 +12,11 @@ namespace Symfony\Bridge\Doctrine\Tests\Form\Type; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\ORM\EntityManager; use Doctrine\ORM\EntityRepository; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ManagerRegistry; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Bridge\Doctrine\Form\DoctrineOrmExtension; use Symfony\Bridge\Doctrine\Form\DoctrineOrmTypeGuesser; use Symfony\Bridge\Doctrine\Form\Type\EntityType; @@ -28,24 +29,29 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; +use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; use Symfony\Component\Form\ChoiceList\View\ChoiceGroupView; use Symfony\Component\Form\ChoiceList\View\ChoiceView; +use Symfony\Component\Form\Exception\RuntimeException; +use Symfony\Component\Form\Exception\UnexpectedTypeException; use Symfony\Component\Form\Forms; use Symfony\Component\Form\Tests\Extension\Core\Type\BaseTypeTest; use Symfony\Component\Form\Tests\Extension\Core\Type\FormTypeTest; +use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException; +use Symfony\Component\OptionsResolver\Exception\MissingOptionsException; class EntityTypeTest extends BaseTypeTest { - const TESTED_TYPE = 'Symfony\Bridge\Doctrine\Form\Type\EntityType'; + public const TESTED_TYPE = 'Symfony\Bridge\Doctrine\Form\Type\EntityType'; - const ITEM_GROUP_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity'; - const SINGLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; - const SINGLE_IDENT_NO_TO_STRING_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity'; - const SINGLE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity'; - const SINGLE_ASSOC_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleAssociationToIntIdEntity'; - const SINGLE_STRING_CASTABLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity'; - const COMPOSITE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'; - const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; + private const ITEM_GROUP_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\GroupableEntity'; + private const SINGLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'; + private const SINGLE_IDENT_NO_TO_STRING_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity'; + private const SINGLE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity'; + private const SINGLE_ASSOC_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleAssociationToIntIdEntity'; + private const SINGLE_STRING_CASTABLE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringCastableIdEntity'; + private const COMPOSITE_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'; + private const COMPOSITE_STRING_IDENT_CLASS = 'Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeStringIdEntity'; /** * @var EntityManager @@ -53,13 +59,11 @@ class EntityTypeTest extends BaseTypeTest private $em; /** - * @var \PHPUnit_Framework_MockObject_MockObject|ManagerRegistry + * @var MockObject&ManagerRegistry */ private $emRegistry; - protected static $supportedFeatureSetVersion = 304; - - protected function setUp() + protected function setUp(): void { $this->em = DoctrineTestHelper::createTestEntityManager(); $this->emRegistry = $this->createRegistryMock('default', $this->em); @@ -89,7 +93,7 @@ protected function setUp() } } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -115,24 +119,53 @@ protected function persist(array $entities) // be managed! } - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\MissingOptionsException - */ public function testClassOptionIsRequired() { + $this->expectException(MissingOptionsException::class); $this->factory->createNamed('name', static::TESTED_TYPE); } - /** - * @expectedException \Symfony\Component\Form\Exception\RuntimeException - */ public function testInvalidClassOption() { + $this->expectException(RuntimeException::class); $this->factory->createNamed('name', static::TESTED_TYPE, null, [ 'class' => 'foo', ]); } + /** + * @dataProvider choiceTranslationDomainProvider + */ + public function testChoiceTranslationDomainIsDisabledByDefault($expanded) + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + + $this->persist([$entity1]); + + $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [ + 'choices' => [ + $entity1, + ], + 'class' => SingleIntIdEntity::class, + 'em' => 'default', + 'expanded' => $expanded, + ]); + + if ($expanded) { + $this->assertFalse($field->get('1')->getConfig()->getOption('translation_domain')); + } else { + $this->assertFalse($field->getConfig()->getOption('choice_translation_domain')); + } + } + + public function choiceTranslationDomainProvider() + { + return [ + [false], + [true], + ]; + } + public function testSetDataToUninitializedEntityWithNonRequired() { $entity1 = new SingleIntIdEntity(1, 'Foo'); @@ -187,23 +220,19 @@ public function testSetDataToUninitializedEntityWithNonRequiredQueryBuilder() $this->assertEquals([1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar')], $view->vars['choices']); } - /** - * @expectedException \Symfony\Component\OptionsResolver\Exception\InvalidOptionsException - */ public function testConfigureQueryBuilderWithNonQueryBuilderAndNonClosure() { - $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [ + $this->expectException(InvalidOptionsException::class); + $this->factory->createNamed('name', static::TESTED_TYPE, null, [ 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, 'query_builder' => new \stdClass(), ]); } - /** - * @expectedException \Symfony\Component\Form\Exception\UnexpectedTypeException - */ public function testConfigureQueryBuilderWithClosureReturningNonQueryBuilder() { + $this->expectException(UnexpectedTypeException::class); $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [ 'em' => 'default', 'class' => self::SINGLE_IDENT_CLASS, @@ -848,7 +877,7 @@ public function testPreferredChoices() ]); $this->assertEquals([3 => new ChoiceView($entity3, '3', 'Baz'), 2 => new ChoiceView($entity2, '2', 'Bar')], $field->createView()->vars['preferred_choices']); - $this->assertEquals([1 => new ChoiceView($entity1, '1', 'Foo')], $field->createView()->vars['choices']); + $this->assertEquals([1 => new ChoiceView($entity1, '1', 'Foo'), 2 => new ChoiceView($entity2, '2', 'Bar'), 3 => new ChoiceView($entity3, '3', 'Baz')], $field->createView()->vars['choices']); } public function testOverrideChoicesWithPreferredChoices() @@ -868,7 +897,7 @@ public function testOverrideChoicesWithPreferredChoices() ]); $this->assertEquals([3 => new ChoiceView($entity3, '3', 'Baz')], $field->createView()->vars['preferred_choices']); - $this->assertEquals([2 => new ChoiceView($entity2, '2', 'Bar')], $field->createView()->vars['choices']); + $this->assertEquals([2 => new ChoiceView($entity2, '2', 'Bar'), 3 => new ChoiceView($entity3, '3', 'Baz')], $field->createView()->vars['choices']); } public function testDisallowChoicesThatAreNotIncludedChoicesSingleIdentifier() @@ -960,6 +989,56 @@ public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleIdentifie $this->assertNull($field->getData()); } + public function testSingleIdentifierWithLimit() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $entity3 = new SingleIntIdEntity(3, 'Baz'); + + $this->persist([$entity1, $entity2, $entity3]); + + $repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS); + + $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => $repository->createQueryBuilder('e') + ->where('e.id IN (1, 2, 3)') + ->setMaxResults(1), + 'choice_label' => 'name', + ]); + + $field->submit('1'); + + $this->assertTrue($field->isSynchronized()); + $this->assertSame($entity1, $field->getData()); + } + + public function testDisallowChoicesThatAreNotIncludedByQueryBuilderSingleIdentifierWithLimit() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $entity3 = new SingleIntIdEntity(3, 'Baz'); + + $this->persist([$entity1, $entity2, $entity3]); + + $repository = $this->em->getRepository(self::SINGLE_IDENT_CLASS); + + $field = $this->factory->createNamed('name', static::TESTED_TYPE, null, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'query_builder' => $repository->createQueryBuilder('e') + ->where('e.id IN (1, 2, 3)') + ->setMaxResults(1), + 'choice_label' => 'name', + ]); + + $field->submit('3'); + + $this->assertFalse($field->isSynchronized()); + $this->assertNull($field->getData()); + } + public function testDisallowChoicesThatAreNotIncludedQueryBuilderSingleAssocIdentifier() { $innerEntity1 = new SingleIntIdNoToStringEntity(1, 'InFoo'); @@ -1166,7 +1245,7 @@ public function testLoaderCaching() $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); - $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1); + $this->assertInstanceOf(ChoiceLoaderInterface::class, $choiceLoader1); $this->assertSame($choiceLoader1, $choiceLoader2); $this->assertSame($choiceLoader1, $choiceLoader3); } @@ -1226,14 +1305,17 @@ public function testLoaderCachingWithParameters() $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); - $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1); + $this->assertInstanceOf(ChoiceLoaderInterface::class, $choiceLoader1); $this->assertSame($choiceLoader1, $choiceLoader2); $this->assertSame($choiceLoader1, $choiceLoader3); } - protected function createRegistryMock($name, $em) + /** + * @return MockObject&ManagerRegistry + */ + protected function createRegistryMock($name, $em): ManagerRegistry { - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->createMock(ManagerRegistry::class); $registry->expects($this->any()) ->method('getManager') ->with($this->equalTo($name)) @@ -1635,7 +1717,7 @@ public function testSetDataEmptyArraySubmitNullMultiple() ]); $form->setData($emptyArray); $form->submit(null); - $this->assertInternalType('array', $form->getData()); + $this->assertIsArray($form->getData()); $this->assertEquals([], $form->getData()); $this->assertEquals([], $form->getNormData()); $this->assertSame([], $form->getViewData(), 'View data is always an array'); @@ -1653,7 +1735,7 @@ public function testSetDataNonEmptyArraySubmitNullMultiple() $existing = [0 => $entity1]; $form->setData($existing); $form->submit(null); - $this->assertInternalType('array', $form->getData()); + $this->assertIsArray($form->getData()); $this->assertEquals([], $form->getData()); $this->assertEquals([], $form->getNormData()); $this->assertSame([], $form->getViewData(), 'View data is always an array'); @@ -1697,4 +1779,32 @@ public function testSubmitNullMultipleUsesDefaultEmptyData() $this->assertEquals($collection, $form->getNormData()); $this->assertEquals($collection, $form->getData()); } + + public function testWithSameLoaderAndDifferentChoiceValueCallbacks() + { + $entity1 = new SingleIntIdEntity(1, 'Foo'); + $entity2 = new SingleIntIdEntity(2, 'Bar'); + $this->persist([$entity1, $entity2]); + + $view = $this->factory->create(FormTypeTest::TESTED_TYPE) + ->add('entity_one', self::TESTED_TYPE, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + ]) + ->add('entity_two', self::TESTED_TYPE, [ + 'em' => 'default', + 'class' => self::SINGLE_IDENT_CLASS, + 'choice_value' => function ($choice) { + return $choice ? $choice->name : ''; + }, + ]) + ->createView() + ; + + $this->assertSame('1', $view['entity_one']->vars['choices'][1]->value); + $this->assertSame('2', $view['entity_one']->vars['choices'][2]->value); + + $this->assertSame('Foo', $view['entity_two']->vars['choices']['Foo']->value); + $this->assertSame('Bar', $view['entity_two']->vars['choices']['Bar']->value); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php b/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php index 10c403f461688..710e87a15e0b8 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Logger/DbalLoggerTest.php @@ -12,8 +12,12 @@ namespace Symfony\Bridge\Doctrine\Tests\Logger; use PHPUnit\Framework\TestCase; +use Psr\Log\LoggerInterface; use Symfony\Bridge\Doctrine\Logger\DbalLogger; +/** + * @group legacy + */ class DbalLoggerTest extends TestCase { /** @@ -21,10 +25,10 @@ class DbalLoggerTest extends TestCase */ public function testLog($sql, $params, $logParams) { - $logger = $this->getMockBuilder('Psr\\Log\\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $dbalLogger = $this - ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') + ->getMockBuilder(DbalLogger::class) ->setConstructorArgs([$logger, null]) ->setMethods(['log']) ->getMock() @@ -45,18 +49,18 @@ public function getLogFixtures() ['SQL', null, []], ['SQL', [], []], ['SQL', ['foo' => 'bar'], ['foo' => 'bar']], - ['SQL', ['foo' => "\x7F\xFF"], ['foo' => DbalLogger::BINARY_DATA_VALUE]], - ['SQL', ['foo' => "bar\x7F\xFF"], ['foo' => DbalLogger::BINARY_DATA_VALUE]], + ['SQL', ['foo' => "\x7F\xFF"], ['foo' => '(binary value)']], + ['SQL', ['foo' => "bar\x7F\xFF"], ['foo' => '(binary value)']], ['SQL', ['foo' => ''], ['foo' => '']], ]; } public function testLogNonUtf8() { - $logger = $this->getMockBuilder('Psr\\Log\\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $dbalLogger = $this - ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') + ->getMockBuilder(DbalLogger::class) ->setConstructorArgs([$logger, null]) ->setMethods(['log']) ->getMock() @@ -76,10 +80,10 @@ public function testLogNonUtf8() public function testLogNonUtf8Array() { - $logger = $this->getMockBuilder('Psr\\Log\\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $dbalLogger = $this - ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') + ->getMockBuilder(DbalLogger::class) ->setConstructorArgs([$logger, null]) ->setMethods(['log']) ->getMock() @@ -107,10 +111,10 @@ public function testLogNonUtf8Array() public function testLogLongString() { - $logger = $this->getMockBuilder('Psr\\Log\\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $dbalLogger = $this - ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') + ->getMockBuilder(DbalLogger::class) ->setConstructorArgs([$logger, null]) ->setMethods(['log']) ->getMock() @@ -135,10 +139,10 @@ public function testLogLongString() public function testLogUTF8LongString() { - $logger = $this->getMockBuilder('Psr\\Log\\LoggerInterface')->getMock(); + $logger = $this->createMock(LoggerInterface::class); $dbalLogger = $this - ->getMockBuilder('Symfony\\Bridge\\Doctrine\\Logger\\DbalLogger') + ->getMockBuilder(DbalLogger::class) ->setConstructorArgs([$logger, null]) ->setMethods(['log']) ->getMock() diff --git a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php index e5ebeeacf813a..dd7dabcc87db1 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/ManagerRegistryTest.php @@ -12,16 +12,20 @@ namespace Symfony\Bridge\Doctrine\Tests; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; +use ProxyManager\Proxy\ValueHolderInterface; use Symfony\Bridge\Doctrine\ManagerRegistry; +use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper\PhpDumperTest; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Dumper\PhpDumper; +use Symfony\Component\Filesystem\Filesystem; class ManagerRegistryTest extends TestCase { - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { - if (!class_exists('PHPUnit_Framework_TestCase')) { - self::markTestSkipped('proxy-manager-bridge is not yet compatible with namespaced phpunit versions.'); - } $test = new PhpDumperTest(); $test->testDumpContainerWithProxyServiceWillShareProxies(); } @@ -42,6 +46,91 @@ public function testResetService() $this->assertSame($foo, $container->get('foo')); $this->assertObjectNotHasAttribute('bar', $foo); } + + /** + * When performing an entity manager lazy service reset, the reset operations may re-use the container + * to create a "fresh" service: when doing so, it can happen that the "fresh" service is itself a proxy. + * + * Because of that, the proxy will be populated with a wrapped value that is itself a proxy: repeating + * the reset operation keeps increasing this nesting until the application eventually runs into stack + * overflow or memory overflow operations, which can happen for long-running processes that rely on + * services that are reset very often. + */ + public function testResetServiceWillNotNestFurtherLazyServicesWithinEachOther() + { + // This test scenario only applies to containers composed as a set of generated sources + $this->dumpLazyServiceProjectAsFilesServiceContainer(); + + /** @var ContainerInterface $container */ + $container = new \LazyServiceProjectAsFilesServiceContainer(); + + $registry = new TestManagerRegistry( + 'irrelevant', + [], + ['defaultManager' => 'foo'], + 'irrelevant', + 'defaultManager', + 'irrelevant' + ); + $registry->setTestContainer($container); + + $service = $container->get('foo'); + + self::assertInstanceOf(\stdClass::class, $service); + self::assertInstanceOf(LazyLoadingInterface::class, $service); + self::assertInstanceOf(ValueHolderInterface::class, $service); + self::assertFalse($service->isProxyInitialized()); + + $service->initializeProxy(); + + self::assertTrue($container->initialized('foo')); + self::assertTrue($service->isProxyInitialized()); + + $registry->resetManager(); + $service->initializeProxy(); + + $wrappedValue = $service->getWrappedValueHolderValue(); + self::assertInstanceOf(\stdClass::class, $wrappedValue); + self::assertNotInstanceOf(LazyLoadingInterface::class, $wrappedValue); + self::assertNotInstanceOf(ValueHolderInterface::class, $wrappedValue); + } + + private function dumpLazyServiceProjectAsFilesServiceContainer() + { + if (class_exists(\LazyServiceProjectAsFilesServiceContainer::class, false)) { + return; + } + + $container = new ContainerBuilder(); + + $container->register('foo', \stdClass::class) + ->setPublic(true) + ->setLazy(true); + $container->compile(); + + $fileSystem = new Filesystem(); + + $temporaryPath = $fileSystem->tempnam(sys_get_temp_dir(), 'symfonyManagerRegistryTest'); + $fileSystem->remove($temporaryPath); + $fileSystem->mkdir($temporaryPath); + + $dumper = new PhpDumper($container); + + $dumper->setProxyDumper(new ProxyDumper()); + $containerFiles = $dumper->dump([ + 'class' => 'LazyServiceProjectAsFilesServiceContainer', + 'as_files' => true, + ]); + + array_walk( + $containerFiles, + static function (string $containerSources, string $fileName) use ($temporaryPath): void { + (new Filesystem())->dumpFile($temporaryPath.'/'.$fileName, $containerSources); + } + ); + + require $temporaryPath.'/LazyServiceProjectAsFilesServiceContainer.php'; + } } class TestManagerRegistry extends ManagerRegistry @@ -51,7 +140,7 @@ public function setTestContainer($container) $this->container = $container; } - public function getAliasNamespace($alias) + public function getAliasNamespace($alias): string { return 'Foo'; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerMiddlewareTest.php deleted file mode 100644 index d20c9cfb50690..0000000000000 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerMiddlewareTest.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\Bridge\Doctrine\Tests\Messenger; - -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\ORM\EntityManagerInterface; -use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerMiddleware; -use Symfony\Component\Messenger\Envelope; -use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; -use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; - -class DoctrineClearEntityManagerMiddlewareTest extends MiddlewareTestCase -{ - public function testMiddlewareClearEntityManager() - { - $entityManager = $this->createMock(EntityManagerInterface::class); - $entityManager->expects($this->once()) - ->method('clear'); - - $managerRegistry = $this->createMock(ManagerRegistry::class); - $managerRegistry - ->method('getManager') - ->with('default') - ->willReturn($entityManager); - - $middleware = new DoctrineClearEntityManagerMiddleware($managerRegistry, 'default'); - - $middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); - } - - public function testInvalidEntityManagerThrowsException() - { - $managerRegistry = $this->createMock(ManagerRegistry::class); - $managerRegistry - ->method('getManager') - ->with('unknown_manager') - ->will($this->throwException(new \InvalidArgumentException())); - - $middleware = new DoctrineClearEntityManagerMiddleware($managerRegistry, 'unknown_manager'); - - $this->expectException(UnrecoverableMessageHandlingException::class); - - $middleware->handle(new Envelope(new \stdClass()), $this->getStackMock(false)); - } -} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerWorkerSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerWorkerSubscriberTest.php new file mode 100644 index 0000000000000..a4cc5ffb87083 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineClearEntityManagerWorkerSubscriberTest.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\Bridge\Doctrine\Tests\Messenger; + +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bridge\Doctrine\Messenger\DoctrineClearEntityManagerWorkerSubscriber; +use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; + +class DoctrineClearEntityManagerWorkerSubscriberTest extends MiddlewareTestCase +{ + public function testMiddlewareClearEntityManager() + { + $entityManager1 = $this->createMock(EntityManagerInterface::class); + $entityManager1->expects($this->once()) + ->method('clear'); + + $entityManager2 = $this->createMock(EntityManagerInterface::class); + $entityManager2->expects($this->once()) + ->method('clear'); + + $managerRegistry = $this->createMock(ManagerRegistry::class); + $managerRegistry + ->method('getManagers') + ->with() + ->willReturn([$entityManager1, $entityManager2]); + + $subscriber = new DoctrineClearEntityManagerWorkerSubscriber($managerRegistry); + $subscriber->onWorkerMessageHandled(); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineCloseConnectionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineCloseConnectionMiddlewareTest.php index df5414e3cc23a..ef5564eca4e95 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineCloseConnectionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineCloseConnectionMiddlewareTest.php @@ -11,12 +11,13 @@ namespace Symfony\Bridge\Doctrine\Tests\Messenger; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Messenger\DoctrineCloseConnectionMiddleware; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; class DoctrineCloseConnectionMiddlewareTest extends MiddlewareTestCase @@ -27,7 +28,7 @@ class DoctrineCloseConnectionMiddlewareTest extends MiddlewareTestCase private $middleware; private $entityManagerName = 'default'; - protected function setUp() + protected function setUp(): void { $this->connection = $this->createMock(Connection::class); @@ -49,7 +50,10 @@ public function testMiddlewareCloseConnection() ->method('close') ; - $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); + $envelope = new Envelope(new \stdClass(), [ + new ConsumedByWorkerStamp(), + ]); + $this->middleware->handle($envelope, $this->getStackMock()); } public function testInvalidEntityManagerThrowsException() @@ -66,4 +70,14 @@ public function testInvalidEntityManagerThrowsException() $middleware->handle(new Envelope(new \stdClass()), $this->getStackMock(false)); } + + public function testMiddlewareNotCloseInNonWorkerContext() + { + $this->connection->expects($this->never()) + ->method('close') + ; + + $envelope = new Envelope(new \stdClass()); + $this->middleware->handle($envelope, $this->getStackMock()); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php index ae71d0d168741..be63ef923dfbc 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrinePingConnectionMiddlewareTest.php @@ -11,12 +11,15 @@ namespace Symfony\Bridge\Doctrine\Tests\Messenger; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; +use Doctrine\DBAL\DBALException; +use Doctrine\DBAL\Exception; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Messenger\DoctrinePingConnectionMiddleware; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; +use Symfony\Component\Messenger\Stamp\ConsumedByWorkerStamp; use Symfony\Component\Messenger\Test\Middleware\MiddlewareTestCase; class DoctrinePingConnectionMiddlewareTest extends MiddlewareTestCase @@ -27,7 +30,7 @@ class DoctrinePingConnectionMiddlewareTest extends MiddlewareTestCase private $middleware; private $entityManagerName = 'default'; - protected function setUp() + protected function setUp(): void { $this->connection = $this->createMock(Connection::class); @@ -46,8 +49,8 @@ protected function setUp() public function testMiddlewarePingOk() { $this->connection->expects($this->once()) - ->method('ping') - ->willReturn(false); + ->method('getDatabasePlatform') + ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); $this->connection->expects($this->once()) ->method('close') @@ -56,11 +59,18 @@ public function testMiddlewarePingOk() ->method('connect') ; - $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); + $envelope = new Envelope(new \stdClass(), [ + new ConsumedByWorkerStamp(), + ]); + $this->middleware->handle($envelope, $this->getStackMock()); } public function testMiddlewarePingResetEntityManager() { + $this->connection->expects($this->once()) + ->method('getDatabasePlatform') + ->will($this->throwException(class_exists(Exception::class) ? new Exception() : new DBALException())); + $this->entityManager->expects($this->once()) ->method('isOpen') ->willReturn(false) @@ -70,7 +80,10 @@ public function testMiddlewarePingResetEntityManager() ->with($this->entityManagerName) ; - $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); + $envelope = new Envelope(new \stdClass(), [ + new ConsumedByWorkerStamp(), + ]); + $this->middleware->handle($envelope, $this->getStackMock()); } public function testInvalidEntityManagerThrowsException() @@ -87,4 +100,24 @@ public function testInvalidEntityManagerThrowsException() $middleware->handle(new Envelope(new \stdClass()), $this->getStackMock(false)); } + + public function testMiddlewareNoPingInNonWorkerContext() + { + // This method has been removed in DBAL 3.0 + if (method_exists(Connection::class, 'ping')) { + $this->connection->expects($this->never()) + ->method('ping') + ->willReturn(false); + } + + $this->connection->expects($this->never()) + ->method('close') + ; + $this->connection->expects($this->never()) + ->method('connect') + ; + + $envelope = new Envelope(new \stdClass()); + $this->middleware->handle($envelope, $this->getStackMock()); + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php index 04fb86140ee37..91094173b6b36 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php @@ -11,9 +11,9 @@ namespace Symfony\Bridge\Doctrine\Tests\Messenger; -use Doctrine\Common\Persistence\ManagerRegistry; use Doctrine\DBAL\Connection; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Bridge\Doctrine\Messenger\DoctrineTransactionMiddleware; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\Exception\UnrecoverableMessageHandlingException; @@ -25,7 +25,7 @@ class DoctrineTransactionMiddlewareTest extends MiddlewareTestCase private $entityManager; private $middleware; - public function setUp() + protected function setUp(): void { $this->connection = $this->createMock(Connection::class); @@ -53,12 +53,10 @@ public function testMiddlewareWrapsInTransactionAndFlushes() $this->middleware->handle(new Envelope(new \stdClass()), $this->getStackMock()); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Thrown from next middleware. - */ public function testTransactionIsRolledBackOnException() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Thrown from next middleware.'); $this->connection->expects($this->once()) ->method('beginTransaction') ; diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php index dd5200117cfea..bd1a26487a6b5 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/DoctrineExtractorTest.php @@ -11,12 +11,20 @@ namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo; +use Doctrine\Common\Collections\Collection; use Doctrine\DBAL\Types\Type as DBALType; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\EntityManager; +use Doctrine\ORM\Mapping\Column; use Doctrine\ORM\Tools\Setup; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy210; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineEnum; use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineGeneratedValue; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt; +use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString; use Symfony\Component\PropertyInfo\Type; /** @@ -49,27 +57,44 @@ public function testLegacyGetProperties() private function doTestGetProperties(bool $legacy) { + // Fields + $expected = [ + 'id', + 'guid', + 'time', + 'timeImmutable', + 'dateInterval', + 'jsonArray', + 'simpleArray', + 'float', + 'decimal', + 'bool', + 'binary', + 'customFoo', + 'bigint', + ]; + + if (class_exists(Types::class)) { + $expected[] = 'json'; + } + + // Associations + $expected = array_merge($expected, [ + 'foo', + 'bar', + 'indexedRguid', + 'indexedBar', + 'indexedFoo', + 'indexedBaz', + 'indexedByDt', + 'indexedByCustomType' F987 , + 'indexedBuz', + 'dummyGeneratedValueList', + ]); + $this->assertEquals( - [ - 'id', - 'guid', - 'time', - 'timeImmutable', - 'dateInterval', - 'json', - 'simpleArray', - 'float', - 'decimal', - 'bool', - 'binary', - 'customFoo', - 'bigint', - 'foo', - 'bar', - 'indexedBar', - 'indexedFoo', - ], - $this->createExtractor($legacy)->getProperties('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy') + $expected, + $this->createExtractor($legacy)->getProperties(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class) ); } @@ -85,7 +110,7 @@ public function testLegacyTestGetPropertiesWithEmbedded() private function doTestGetPropertiesWithEmbedded(bool $legacy) { - if (!class_exists('Doctrine\ORM\Mapping\Embedded')) { + if (!class_exists(\Doctrine\ORM\Mapping\Embedded::class)) { $this->markTestSkipped('@Embedded is not available in Doctrine ORM lower than 2.5.'); } @@ -116,7 +141,7 @@ public function testLegacyExtract($property, array $type = null) private function doTestExtract(bool $legacy, $property, array $type = null) { - $this->assertEquals($type, $this->createExtractor($legacy)->getTypes('Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy', $property, [])); + $this->assertEquals($type, $this->createExtractor($legacy)->getTypes(!class_exists(Types::class) ? 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy' : DoctrineDummy210::class, $property, [])); } public function testExtractWithEmbedded() @@ -131,7 +156,7 @@ public function testLegacyExtractWithEmbedded() private function doTestExtractWithEmbedded(bool $legacy) { - if (!class_exists('Doctrine\ORM\Mapping\Embedded')) { + if (!class_exists(\Doctrine\ORM\Mapping\Embedded::class)) { $this->markTestSkipped('@Embedded is not available in Doctrine ORM lower than 2.5.'); } @@ -150,9 +175,24 @@ private function doTestExtractWithEmbedded(bool $legacy) $this->assertEquals($expectedTypes, $actualTypes); } + /** + * @requires PHP 8.1 + */ + public function testExtractEnum() + { + if (!property_exists(Column::class, 'enumType')) { + $this->markTestSkipped('The "enumType" requires doctrine/orm 2.11.'); + } + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', [])); + $this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', [])); + $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumStringArray', [])); + $this->assertEquals([new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class))], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumIntArray', [])); + $this->assertNull($this->createExtractor()->getTypes(DoctrineEnum::class, 'enumCustom', [])); + } + public function typesProvider() { - return [ + $provider = [ ['id', [new Type(Type::BUILTIN_TYPE_INT)]], ['guid', [new Type(Type::BUILTIN_TYPE_STRING)]], ['bigint', [new Type(Type::BUILTIN_TYPE_STRING)]], @@ -163,7 +203,7 @@ public function typesProvider() ['decimal', [new Type(Type::BUILTIN_TYPE_STRING)]], ['bool', [new Type(Type::BUILTIN_TYPE_BOOL)]], ['binary', [new Type(Type::BUILTIN_TYPE_RESOURCE)]], - ['json', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], + ['jsonArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true)]], ['foo', [new Type(Type::BUILTIN_TYPE_OBJECT, true, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation')]], ['bar', [new Type( Type::BUILTIN_TYPE_OBJECT, @@ -173,6 +213,14 @@ public function typesProvider() new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], + ['indexedRguid', [new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + 'Doctrine\Common\Collections\Collection', + true, + new Type(Type::BUILTIN_TYPE_STRING), + new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') + )]], ['indexedBar', [new Type( Type::BUILTIN_TYPE_OBJECT, false, @@ -189,10 +237,50 @@ public function typesProvider() new Type(Type::BUILTIN_TYPE_STRING), new Type(Type::BUILTIN_TYPE_OBJECT, false, 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation') )]], + ['indexedBaz', [new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + Collection::class, + true, + new Type(Type::BUILTIN_TYPE_INT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + )]], ['simpleArray', [new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, new Type(Type::BUILTIN_TYPE_INT), new Type(Type::BUILTIN_TYPE_STRING))]], ['customFoo', null], ['notMapped', null], + ['indexedByDt', [new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + Collection::class, + true, + new Type(Type::BUILTIN_TYPE_OBJECT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + )]], + ['indexedByCustomType', null], + ['indexedBuz', [new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + Collection::class, + true, + new Type(Type::BUILTIN_TYPE_STRING), + new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + )]], + ['dummyGeneratedValueList', [new Type( + Type::BUILTIN_TYPE_OBJECT, + false, + 'Doctrine\Common\Collections\Collection', + true, + new Type(Type::BUILTIN_TYPE_INT), + new Type(Type::BUILTIN_TYPE_OBJECT, false, DoctrineRelation::class) + )]], + ['json', null], ]; + + if (class_exists(Types::class)) { + $provider[] = ['json', null]; + } + + return $provider; } public function testGetPropertiesCatchException() diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php index d02deb15bb7b5..67d61f2abfd3d 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy.php @@ -44,6 +44,11 @@ class DoctrineDummy /** * @ManyToMany(targetEntity="DoctrineRelation", indexBy="rguid") */ + protected $indexedRguid; + + /** + * @ManyToMany(targetEntity="DoctrineRelation", indexBy="rguid_column") + */ protected $indexedBar; /** @@ -51,6 +56,11 @@ class DoctrineDummy */ protected $indexedFoo; + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="baz", indexBy="baz_id") + */ + protected $indexedBaz; + /** * @Column(type="guid") */ @@ -74,7 +84,7 @@ class DoctrineDummy /** * @Column(type="json_array") */ - private $json; + private $jsonArray; /** * @Column(type="simple_array") @@ -112,4 +122,24 @@ class DoctrineDummy private $bigint; public $notMapped; + + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="dt", indexBy="dt") + */ + protected $indexedByDt; + + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="customType", indexBy="customType") + */ + private $indexedByCustomType; + + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="buzField", indexBy="buzField") + */ + protected $indexedBuz; + + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="dummyRelation", indexBy="gen_value_col_id", orphanRemoval=true) + */ + protected $dummyGeneratedValueList; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.php new file mode 100644 index 0000000000000..d3916143deab7 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineDummy210.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\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\ManyToMany; +use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\OneToMany; + +/** + * @Entity + */ +final class DoctrineDummy210 extends DoctrineDummy +{ + /** + * @Column(type="json", nullable=true) + */ + private $json; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineEnum.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineEnum.php new file mode 100644 index 0000000000000..fd5271fc47730 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineEnum.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\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\Column; +use Doctrine\ORM\Mapping\Entity; + +/** + * @Entity + */ +class DoctrineEnum +{ + /** + * @Id + * @Column(type="smallint") + */ + public $id; + + /** + * @Column(type="string", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString") + */ + protected $enumString; + + /** + * @Column(type="integer", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt") + */ + protected $enumInt; + + /** + * @Column(type="array", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString") + */ + protected $enumStringArray; + + /** + * @Column(type="simple_array", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt") + */ + protected $enumIntArray; + + /** + * @Column(type="custom_foo", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt") + */ + protected $enumCustom; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php index 1b8cba50f3ece..d7dbb1eeb41f9 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php @@ -23,12 +23,12 @@ class DoctrineFooType extends Type /** * Type name. */ - const NAME = 'foo'; + private const NAME = 'foo'; /** * {@inheritdoc} */ - public function getName() + public function getName(): string { return self::NAME; } @@ -36,21 +36,23 @@ public function getName() /** * {@inheritdoc} */ - public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform) + public function getSQLDeclaration(array $fieldDeclaration, AbstractPlatform $platform): string { return $platform->getClobTypeDeclarationSQL([]); } /** * {@inheritdoc} + * + * @return mixed */ public function convertToDatabaseValue($value, AbstractPlatform $platform) { if (null === $value) { - return; + return null; } if (!$value instanceof Foo) { - throw new ConversionException(sprintf('Expected %s, got %s', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', \gettype($value))); + throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', \gettype($value))); } return $foo->bar; @@ -58,11 +60,13 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) /** * {@inheritdoc} + * + * @return mixed */ public function convertToPHPValue($value, AbstractPlatform $platform) { if (null === $value) { - return; + return null; } if (!\is_string($value)) { throw ConversionException::conversionFailed($value, self::NAME); @@ -77,7 +81,7 @@ public function convertToPHPValue($value, AbstractPlatform $platform) /** * {@inheritdoc} */ - public function requiresSQLCommentHint(AbstractPlatform $platform) + public function requiresSQLCommentHint(AbstractPlatform $platform): bool { return true; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineGeneratedValue.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineGeneratedValue.php index 8418b5e5912fb..9e7612fa35ae4 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineGeneratedValue.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineGeneratedValue.php @@ -15,6 +15,7 @@ use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\GeneratedValue; use Doctrine\ORM\Mapping\Id; +use Doctrine\ORM\Mapping\OneToMany; /** * @author KΓ©vin Dunglas @@ -34,4 +35,15 @@ class DoctrineGeneratedValue * @Column */ public $foo; + + /** + * @var int + * @Column(type="integer", name="gen_value_col_id") + */ + public $valueId; + + /** + * @OneToMany(targetEntity="DoctrineRelation", mappedBy="generatedValueRelation", indexBy="rguid_column", orphanRemoval=true) + */ + protected $relationList; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php index 85660d3d6b66c..61a658096add0 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineRelation.php @@ -15,6 +15,7 @@ use Doctrine\ORM\Mapping\Entity; use Doctrine\ORM\Mapping\Id; use Doctrine\ORM\Mapping\ManyToOne; +use Doctrine\ORM\Mapping\JoinColumn; /** * @Entity @@ -30,7 +31,7 @@ class DoctrineRelation public $id; /** - * @Column(type="guid") + * @Column(type="guid", name="rguid_column") */ protected $rguid; @@ -39,4 +40,36 @@ class DoctrineRelation * @ManyToOne(targetEntity="DoctrineDummy", inversedBy="indexedFoo") */ protected $foo; + + /** + * @ManyToOne(targetEntity="DoctrineDummy") + */ + protected $baz; + + /** + * @Column(type="datetime") + */ + private $dt; + + /** + * @Column(type="foo") + */ + private $customType; + + /** + * @Column(type="guid", name="different_than_field") + * @ManyToOne(targetEntity="DoctrineDummy", inversedBy="indexedBuz") + */ + protected $buzField; + + /** + * @ManyToOne(targetEntity="DoctrineDummy", inversedBy="dummyGeneratedValueList") + */ + private $dummyRelation; + + /** + * @ManyToOne(targetEntity="DoctrineGeneratedValue", inversedBy="relationList") + * @JoinColumn(name="gen_value_col_id", referencedColumnName="gen_value_col_id") + */ + private $generatedValueRelation; } diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumInt.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumInt.php new file mode 100644 index 0000000000000..c9560073fa611 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumInt.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\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +enum EnumInt: int +{ + case Foo = 0; + case Bar = 1; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumString.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumString.php new file mode 100644 index 0000000000000..0b6ef0df1bd41 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/EnumString.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\Bridge\Doctrine\Tests\PropertyInfo\Fixtures; + +enum EnumString: string +{ + case Foo = 'f'; + case Bar = 'b'; +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.php new file mode 100644 index 0000000000000..0e3e9aa9090e2 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/RememberMe/DoctrineTokenProviderTest.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 Security\RememberMe; + +use Doctrine\DBAL\DriverManager; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\Security\RememberMe\DoctrineTokenProvider; +use Symfony\Component\Security\Core\Authentication\RememberMe\PersistentToken; +use Symfony\Component\Security\Core\Exception\TokenNotFoundException; + +/** + * @requires extension pdo_sqlite + */ +class DoctrineTokenProviderTest extends TestCase +{ + public function testCreateNewToken() + { + $provider = $this->bootstrapProvider(); + + $token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51')); + $provider->createNewToken($token); + + $this->assertEquals($provider->loadTokenBySeries('someSeries'), $token); + } + + public function testLoadTokenBySeriesThrowsNotFoundException() + { + $provider = $this->bootstrapProvider(); + + $this->expectException(TokenNotFoundException::class); + $provider->loadTokenBySeries('someSeries'); + } + + public function testUpdateToken() + { + $provider = $this->bootstrapProvider(); + + $token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51')); + $provider->createNewToken($token); + $provider->updateToken('someSeries', 'newValue', $lastUsed = new \DateTime('2014-06-26T22:03:46')); + $token = $provider->loadTokenBySeries('someSeries'); + + $this->assertEquals('newValue', $token->getTokenValue()); + $this->assertEquals($token->getLastUsed(), $lastUsed); + } + + public function testDeleteToken() + { + $provider = $this->bootstrapProvider(); + $token = new PersistentToken('someClass', 'someUser', 'someSeries', 'tokenValue', new \DateTime('2013-01-26T18:23:51')); + $provider->createNewToken($token); + $provider->deleteTokenBySeries('someSeries'); + + $this->expectException(TokenNotFoundException::class); + + $provider->loadTokenBySeries('someSeries'); + } + + /** + * @return DoctrineTokenProvider + */ + private function bootstrapProvider() + { + $connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'url' => 'sqlite:///:memory:', + ]); + $connection->{method_exists($connection, 'executeStatement') ? 'executeStatement' : 'executeUpdate'}(<<< 'SQL' + CREATE TABLE rememberme_token ( + series char(88) UNIQUE PRIMARY KEY NOT NULL, + value char(88) NOT NULL, + lastUsed datetime NOT NULL, + class varchar(100) NOT NULL, + username varchar(200) NOT NULL + ); +SQL + ); + + return new DoctrineTokenProvider($connection); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php index 0b616a588f9ca..845515b901155 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Security/User/EntityUserProviderTest.php @@ -11,11 +11,19 @@ namespace Symfony\Bridge\Doctrine\Tests\Security\User; +use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Security\User\EntityUserProvider; +use Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\User; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\PasswordUpgraderInterface; +use Symfony\Component\Security\Core\User\UserInterface; class EntityUserProviderTest extends TestCase { @@ -58,18 +66,14 @@ public function testLoadUserByUsernameWithUserLoaderRepositoryAndWithoutProperty { $user = new User(1, 1, 'user1'); - $repository = $this->getMockBuilder('Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface') - ->disableOriginalConstructor() - ->getMock(); + $repository = $this->createMock(UserLoaderRepository::class); $repository ->expects($this->once()) ->method('loadUserByUsername') ->with('user1') ->willReturn($user); - $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') - ->disableOriginalConstructor() - ->getMock(); + $em = $this->createMock(EntityManager::class); $em ->expects($this->once()) ->method('getRepository') @@ -80,12 +84,10 @@ public function testLoadUserByUsernameWithUserLoaderRepositoryAndWithoutProperty $this->assertSame($user, $provider->loadUserByUsername('user1')); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage You must either make the "Symfony\Bridge\Doctrine\Tests\Fixtures\User" entity Doctrine Repository ("Doctrine\ORM\EntityRepository") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration. - */ public function testLoadUserByUsernameWithNonUserLoaderRepositoryAndWithoutProperty() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('You must either make the "Symfony\Bridge\Doctrine\Tests\Fixtures\User" entity Doctrine Repository ("Doctrine\ORM\EntityRepository") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.'); $em = DoctrineTestHelper::createTestEntityManager(); $this->createSchema($em); @@ -105,10 +107,8 @@ public function testRefreshUserRequiresId() $user1 = new User(null, null, 'user1'); $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User', 'name'); - $this->expectException( - 'InvalidArgumentException', - 'You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine' - ); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('You cannot refresh a user from the EntityUserProvider that does not contain an identifier. The user object has to be serialized with its own identifier mapped by Doctrine'); $provider->refreshUser($user1); } @@ -125,10 +125,9 @@ public function testRefreshInvalidUser() $provider = new EntityUserProvider($this->getManager($em), 'Symfony\Bridge\Doctrine\Tests\Fixtures\User', 'name'); $user2 = new User(1, 2, 'user2'); - $this->expectException( - 'Symfony\Component\Security\Core\Exception\UsernameNotFoundException', - 'User with id {"id1":1,"id2":2} not found' - ); + $this->expectException(UsernameNotFoundException::class); + $this->expectExceptionMessage('User with id {"id1":1,"id2":2} not found'); + $provider->refreshUser($user2); } @@ -151,12 +150,12 @@ public function testSupportProxy() public function testLoadUserByUserNameShouldLoadUserWhenProperInterfaceProvided() { - $repository = $this->getMockBuilder('\Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface')->getMock(); + $repository = $this->createMock(UserLoaderRepository::class); $repository->expects($this->once()) ->method('loadUserByUsername') ->with('name') ->willReturn( - $this->getMockBuilder('\Symfony\Component\Security\Core\User\UserInterface')->getMock() + $this->createMock(UserInterface::class) ); $provider = new EntityUserProvider( @@ -167,12 +166,10 @@ public function testLoadUserByUserNameShouldLoadUserWhenProperInterfaceProvided( $provider->loadUserByUsername('name'); } - /** - * @expectedException \InvalidArgumentException - */ public function testLoadUserByUserNameShouldDeclineInvalidInterface() { - $repository = $this->getMockBuilder('\Symfony\Component\Security\Core\User\AdvancedUserInterface')->getMock(); + $this->expectException(\InvalidArgumentException::class); + $repository = $this->createMock(ObjectRepository::class); $provider = new EntityUserProvider( $this->getManager($this->getObjectManager($repository)), @@ -182,9 +179,26 @@ public function testLoadUserByUserNameShouldDeclineInvalidInterface() $provider->loadUserByUsername('name'); } + public function testPasswordUpgrades() + { + $user = new User(1, 1, 'user1'); + + $repository = $this->createMock(PasswordUpgraderRepository::class); + $repository->expects($this->once()) + ->method('upgradePassword') + ->with($user, 'foobar'); + + $provider = new EntityUserProvider( + $this->getManager($this->getObjectManager($repository)), + 'Symfony\Bridge\Doctrine\Tests\Fixtures\User' + ); + + $provider->upgradePassword($user, 'foobar'); + } + private function getManager($em, $name = null) { - $manager = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $manager = $this->createMock(ManagerRegistry::class); $manager->expects($this->any()) ->method('getManager') ->with($this->equalTo($name)) @@ -195,7 +209,7 @@ private function getManager($em, $name = null) private function getObjectManager($repository) { - $em = $this->getMockBuilder('\Doctrine\Common\Persistence\ObjectManager') + $em = $this->getMockBuilder(ObjectManager::class) ->setMethods(['getClassMetadata', 'getRepository']) ->getMockForAbstractClass(); $em->expects($this->any()) @@ -213,3 +227,14 @@ private function createSchema($em) ]); } } + +abstract class UserLoaderRepository implements ObjectRepository, UserLoaderInterface +{ +} + +abstract class PasswordUpgraderRepository implements ObjectRepository, PasswordUpgraderInterface +{ + public function upgradePassword(UserInterface $user, string $newEncodedPassword): void + { + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php index 24a7f555f4f67..3032e41f132ea 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php @@ -12,15 +12,17 @@ namespace Symfony\Bridge\Doctrine\Tests\Validator\Constraints; use Doctrine\Common\Collections\ArrayCollection; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\ObjectManager; -use Doctrine\Common\Persistence\ObjectRepository; use Doctrine\DBAL\Types\Type; +use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Tools\SchemaTool; +use Doctrine\Persistence\ManagerRegistry; +use Doctrine\Persistence\ObjectManager; +use Doctrine\Persistence\ObjectRepository; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Test\TestRepositoryFactory; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2; +use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNullableNameEntity; @@ -29,9 +31,13 @@ use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdStringWrapperNameEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapper; +use Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapperType; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntityValidator; +use Symfony\Component\Validator\Exception\ConstraintDefinitionException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; use Symfony\Component\Validator\Test\ConstraintValidatorTestCase; /** @@ -39,7 +45,7 @@ */ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase { - const EM_NAME = 'foo'; + private const EM_NAME = 'foo'; /** * @var ObjectManager @@ -58,7 +64,7 @@ class UniqueEntityValidatorTest extends ConstraintValidatorTestCase protected $repositoryFactory; - protected function setUp() + protected function setUp(): void { $this->repositoryFactory = new TestRepositoryFactory(); @@ -66,7 +72,7 @@ protected function setUp() $config->setRepositoryFactory($this->repositoryFactory); if (!Type::hasType('string_wrapper')) { - Type::addType('string_wrapper', 'Symfony\Bridge\Doctrine\Tests\Fixtures\Type\StringWrapperType'); + Type::addType('string_wrapper', StringWrapperType::class); } $this->em = DoctrineTestHelper::createTestEntityManager($config); @@ -76,9 +82,9 @@ protected function setUp() parent::setUp(); } - protected function createRegistryMock(ObjectManager $em = null) + protected function createRegistryMock($em = null) { - $registry = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $registry = $this->createMock(ManagerRegistry::class); $registry->expects($this->any()) ->method('getManager') ->with($this->equalTo(self::EM_NAME)) @@ -89,7 +95,7 @@ protected function createRegistryMock(ObjectManager $em = null) protected function createRepositoryMock() { - $repository = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectRepository') + $repository = $this->getMockBuilder(ObjectRepository::class) ->setMethods(['findByCustom', 'find', 'findAll', 'findOneBy', 'findBy', 'getClassName']) ->getMock() ; @@ -99,31 +105,20 @@ protected function createRepositoryMock() protected function createEntityManagerMock($repositoryMock) { - $em = $this->getMockBuilder('Doctrine\Common\Persistence\ObjectManager') - ->getMock() - ; + $em = $this->createMock(ObjectManager::class); $em->expects($this->any()) ->method('getRepository') ->willReturn($repositoryMock) ; - $classMetadata = $this->getMockBuilder('Doctrine\Common\Persistence\Mapping\ClassMetadata')->getMock(); + $classMetadata = $this->createMock(ClassMetadataInfo::class); $classMetadata ->expects($this->any()) ->method('hasField') ->willReturn(true) ; - $reflParser = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionParser') - ->disableOriginalConstructor() - ->getMock() - ; - $refl = $this->getMockBuilder('Doctrine\Common\Reflection\StaticReflectionProperty') - ->setConstructorArgs([$reflParser, 'property-name']) - ->setMethods(['getValue']) - ->getMock() - ; + $refl = $this->createMock(\ReflectionProperty::class); $refl - ->expects($this->any()) ->method('getValue') ->willReturn(true) ; @@ -141,21 +136,21 @@ protected function createValidator() return new UniqueEntityValidator($this->registry); } - private function createSchema(ObjectManager $em) + private function createSchema($em) { $schemaTool = new SchemaTool($em); $schemaTool->createSchema([ - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdNoToStringEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNameEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\DoubleNullableNameEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\Person'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\Employee'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeObjectNoToStringIdEntity'), - $em->getClassMetadata('Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdStringWrapperNameEntity'), + $em->getClassMetadata(SingleIntIdEntity::class), + $em->getClassMetadata(SingleIntIdNoToStringEntity::class), + $em->getClassMetadata(DoubleNameEntity::class), + $em->getClassMetadata(DoubleNullableNameEntity::class), + $em->getClassMetadata(CompositeIntIdEntity::class), + $em->getClassMetadata(AssociationEntity::class), + $em->getClassMetadata(AssociationEntity2::class), + $em->getClassMetadata(Person::class), + $em->getClassMetadata(Employee::class), + $em->getClassMetadata(CompositeObjectNoToStringIdEntity::class), + $em->getClassMetadata(SingleIntIdStringWrapperNameEntity::class), ]); } @@ -275,11 +270,9 @@ public function testValidateUniquenessWithIgnoreNullDisabled() ->assertRaised(); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - */ public function testAllConfiguredFieldsAreCheckedOfBeingMappedByDoctrineWithIgnoreNullEnabled() { + $this->expectException(ConstraintDefinitionException::class); $constraint = new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name', 'name2'], @@ -550,7 +543,7 @@ public function testAssociatedEntityWithNull() public function testValidateUniquenessWithArrayValue() { $repository = $this->createRepositoryMock(); - $this->repositoryFactory->setRepository($this->em, 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity', $repository); + $this->repositoryFactory->setRepository($this->em, SingleIntIdEntity::class, $repository); $constraint = new UniqueEntity([ 'message' => 'myMessage', @@ -586,12 +579,10 @@ public function testValidateUniquenessWithArrayValue() ->assertRaised(); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - * @expectedExceptionMessage Object manager "foo" does not exist. - */ public function testDedicatedEntityManagerNullObject() { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('Object manager "foo" does not exist.'); $constraint = new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], @@ -608,12 +599,10 @@ public function testDedicatedEntityManagerNullObject() $this->validator->validate($entity, $constraint); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - * @expectedExceptionMessage Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity" - */ public function testEntityManagerNullObject() { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('Unable to find the object manager associated with an entity of class "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleIntIdEntity"'); $constraint = new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], @@ -664,7 +653,7 @@ public function testValidateInheritanceUniqueness() 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - 'entityClass' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\Person', + 'entityClass' => Person::class, ]); $entity1 = new Person(1, 'Foo'); @@ -692,17 +681,15 @@ public function testValidateInheritanceUniqueness() ->assertRaised(); } - /** - * @expectedException \Symfony\Component\Validator\Exception\ConstraintDefinitionException - * @expectedExceptionMessage The "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity" entity repository does not support the "Symfony\Bridge\Doctrine\Tests\Fixtures\Person" entity. The entity should be an instance of or extend "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity". - */ public function testInvalidateRepositoryForInheritance() { + $this->expectException(ConstraintDefinitionException::class); + $this->expectExceptionMessage('The "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity" entity repository does not support the "Symfony\Bridge\Doctrine\Tests\Fixtures\Person" entity. The entity should be an instance of or extend "Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity".'); $constraint = new UniqueEntity([ 'message' => 'myMessage', 'fields' => ['name'], 'em' => self::EM_NAME, - 'entityClass' => 'Symfony\Bridge\Doctrine\Tests\Fixtures\SingleStringIdEntity', + 'entityClass' => SingleStringIdEntity::class, ]); $entity = new Person(1, 'Foo'); @@ -807,4 +794,129 @@ public function testValidateUniquenessCause() ->setCode(UniqueEntity::NOT_UNIQUE_ERROR) ->assertRaised(); } + + /** + * @dataProvider resultWithEmptyIterator + */ + public function testValidateUniquenessWithEmptyIterator($entity, $result) + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + 'repositoryMethod' => 'findByCustom', + ]); + + $repository = $this->createRepositoryMock(); + $repository->expects($this->once()) + ->method('findByCustom') + ->willReturn($result) + ; + $this->em = $this->createEntityManagerMock($repository); + $this->registry = $this->createRegistryMock($this->em); + $this->validator = $this->createValidator(); + $this->validator->initialize($this->context); + + $this->validator->validate($entity, $constraint); + + $this->assertNoViolation(); + } + + public function testValueMustBeObject() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + ]); + + $this->expectException(UnexpectedValueException::class); + + $this->validator->validate('foo', $constraint); + } + + public function testValueCanBeNull() + { + $constraint = new UniqueEntity([ + 'message' => 'myMessage', + 'fields' => ['name'], + 'em' => self::EM_NAME, + ]); + + $this->validator->validate(null, $constraint); + + $this->assertNoViolation(); + } + + public function resultWithEmptyIterator(): array + { + $entity = new SingleIntIdEntity(1, 'foo'); + + return [ + [$entity, new class() implements \Iterator { + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return null; + } + + public function valid(): bool + { + return false; + } + + public function next(): void + { + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return false; + } + + public function rewind(): void + { + } + }], + [$entity, new class() implements \Iterator { + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function current() + { + return false; + } + + public function valid(): bool + { + return false; + } + + public function next(): void + { + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function key() + { + return false; + } + + public function rewind(): void + { + } + }], + ]; + } } diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php index 45cae2da414f1..0bdc2efc2a77a 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/DoctrineLoaderTest.php @@ -11,34 +11,42 @@ namespace Symfony\Bridge\Doctrine\Tests\Validator; +use Doctrine\ORM\Mapping\Column; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Doctrine\Test\DoctrineTestHelper; use Symfony\Bridge\Doctrine\Tests\Fixtures\BaseUser; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity; +use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEnum; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed; +use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity; use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Bridge\Doctrine\Validator\DoctrineLoader; use Symfony\Component\Validator\Constraints\Length; +use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\CascadingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait; +use Symfony\Component\Validator\Mapping\PropertyMetadata; use Symfony\Component\Validator\Mapping\TraversalStrategy; use Symfony\Component\Validator\Tests\Fixtures\Entity; use Symfony\Component\Validator\Validation; -use Symfony\Component\Validator\ValidatorBuilder; /** * @author KΓ©vin Dunglas */ class DoctrineLoaderTest extends TestCase { - public function testLoadClassMetadata() + protected function setUp(): void { - if (!method_exists(ValidatorBuilder::class, 'addLoader')) { - $this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+'); + if (!trait_exists(AutoMappingTrait::class)) { + $this->markTestSkipped('Auto-mapping requires symfony/validation 4.4+'); } + } + public function testLoadClassMetadata() + { $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') ->enableAnnotationMapping() @@ -134,14 +142,42 @@ public function testLoadClassMetadata() $this->assertCount(1, $textFieldConstraints); $this->assertInstanceOf(Length::class, $textFieldConstraints[0]); $this->assertSame(1000, $textFieldConstraints[0]->max); + + /** @var PropertyMetadata[] $noAutoMappingMetadata */ + $noAutoMappingMetadata = $classMetadata->getPropertyMetadata('noAutoMapping'); + $this->assertCount(1, $noAutoMappingMetadata); + $noAutoMappingConstraints = $noAutoMappingMetadata[0]->getConstraints(); + $this->assertCount(0, $noAutoMappingConstraints); + $this->assertSame(AutoMappingStrategy::DISABLED, $noAutoMappingMetadata[0]->getAutoMappingStrategy()); } - public function testFieldMappingsConfiguration() + /** + * @requires PHP 8.1 + */ + public function testExtractEnum() { - if (!method_exists(ValidatorBuilder::class, 'addLoader')) { - $this->markTestSkipped('Auto-mapping requires symfony/validation 4.2+'); + if (!property_exists(Column::class, 'enumType')) { + $this->markTestSkipped('The "enumType" requires doctrine/orm 2.11.'); } + $validator = Validation::createValidatorBuilder() + ->addMethodMapping('loadValidatorMetadata') + ->enableAnnotationMapping() + ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}')) + ->getValidator() + ; + + $classMetadata = $validator->getMetadataFor(new DoctrineLoaderEnum()); + + $enumStringMetadata = $classMetadata->getPropertyMetadata('enumString'); + $this->assertCount(0, $enumStringMetadata); // asserts the length constraint is not added to an enum + + $enumStringMetadata = $classMetadata->getPropertyMetadata('enumInt'); + $this->assertCount(0, $enumStringMetadata); // asserts the length constraint is not added to an enum + } + + public function testFieldMappingsConfiguration() + { $validator = Validation::createValidatorBuilder() ->addMethodMapping('loadValidatorMetadata') ->enableAnnotationMapping() @@ -166,7 +202,7 @@ public function testFieldMappingsConfiguration() */ public function testClassValidator(bool $expected, string $classValidatorRegexp = null) { - $doctrineLoader = new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), $classValidatorRegexp); + $doctrineLoader = new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), $classValidatorRegexp, false); $classMetadata = new ClassMetadata(DoctrineLoaderEntity::class); $this->assertSame($expected, $doctrineLoader->loadClassMetadata($classMetadata)); @@ -176,8 +212,31 @@ public function regexpProvider() { return [ [false, null], + [true, '{.*}'], [true, '{^'.preg_quote(DoctrineLoaderEntity::class).'$|^'.preg_quote(Entity::class).'$}'], [false, '{^'.preg_quote(Entity::class).'$}'], ]; } + + public function testClassNoAutoMapping() + { + $validator = Validation::createValidatorBuilder() + ->enableAnnotationMapping() + ->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{.*}')) + ->getValidator(); + + $classMetadata = $validator->getMetadataFor(new DoctrineLoaderNoAutoMappingEntity()); + + $classConstraints = $classMetadata->getConstraints(); + $this->assertCount(0, $classConstraints); + $this->assertSame(AutoMappingStrategy::DISABLED, $classMetadata->getAutoMappingStrategy()); + + $maxLengthMetadata = $classMetadata->getPropertyMetadata('maxLength'); + $this->assertEmpty($maxLengthMetadata); + + /** @var PropertyMetadata[] $autoMappingExplicitlyEnabledMetadata */ + $autoMappingExplicitlyEnabledMetadata = $classMetadata->getPropertyMetadata('autoMappingExplicitlyEnabled'); + $this->assertCount(1, $autoMappingExplicitlyEnabledMetadata[0]->constraints); + $this->assertSame(AutoMappingStrategy::ENABLED, $autoMappingExplicitlyEnabledMetadata[0]->getAutoMappingStrategy()); + } } diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php index 2c319709ebc9d..3839f14f35330 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntity.php @@ -23,7 +23,7 @@ */ class UniqueEntity extends Constraint { - const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; + public const NOT_UNIQUE_ERROR = '23bd9dbf-6b9b-41cd-a99e-4844bcf3077f'; public $message = 'This value is already used.'; public $service = 'doctrine.orm.validator.unique'; diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index cf0ed8c962101..cce8cb9079723 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -11,13 +11,12 @@ namespace Symfony\Bridge\Doctrine\Validator\Constraints; -use Doctrine\Common\Persistence\ManagerRegistry; -use Doctrine\Common\Persistence\Mapping\ClassMetadata; -use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\Exception\ConstraintDefinitionException; use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Exception\UnexpectedValueException; /** * Unique Entity Validator checks if one or a set of fields contain unique values. @@ -34,8 +33,7 @@ public function __construct(ManagerRegistry $registry) } /** - * @param object $entity - * @param Constraint $constraint + * @param object $entity * * @throws UnexpectedTypeException * @throws ConstraintDefinitionException @@ -43,7 +41,7 @@ public function __construct(ManagerRegistry $registry) public function validate($entity, Constraint $constraint) { if (!$constraint instanceof UniqueEntity) { - throw new UnexpectedTypeException($constraint, __NAMESPACE__.'\UniqueEntity'); + throw new UnexpectedTypeException($constraint, UniqueEntity::class); } if (!\is_array($constraint->fields) && !\is_string($constraint->fields)) { @@ -64,6 +62,10 @@ public function validate($entity, Constraint $constraint) return; } + if (!\is_object($entity)) { + throw new UnexpectedValueException($entity, 'object'); + } + if ($constraint->em) { $em = $this->registry->getManager($constraint->em); @@ -79,7 +81,6 @@ public function validate($entity, Constraint $constraint) } $class = $em->getClassMetadata(\get_class($entity)); - /* @var $class \Doctrine\Common\Persistence\Mapping\ClassMetadata */ $criteria = []; $hasNullValue = false; @@ -151,8 +152,7 @@ public function validate($entity, Constraint $constraint) if ($result instanceof \Countable && 1 < \count($result)) { $result = [$result->current(), $result->current()]; } else { - $result = $result->current(); - $result = null === $result ? [] : [$result]; + $result = $result->valid() && null !== $result->current() ? [$result->current()] : []; } } elseif (\is_array($result)) { reset($result); @@ -168,8 +168,8 @@ public function validate($entity, Constraint $constraint) return; } - $errorPath = null !== $constraint->errorPath ? $constraint->errorPath : $fields[0]; - $invalidValue = isset($criteria[$errorPath]) ? $criteria[$errorPath] : $criteria[$fields[0]]; + $errorPath = $constraint->errorPath ?? $fields[0]; + $invalidValue = $criteria[$errorPath] ?? $criteria[$fields[0]]; $this->context->buildViolation($constraint->message) ->atPath($errorPath) @@ -180,7 +180,7 @@ public function validate($entity, Constraint $constraint) ->addViolation(); } - private function formatWithIdentifiers(ObjectManager $em, ClassMetadata $class, $value) + private function formatWithIdentifiers($em, $class, $value) { if (!\is_object($value) || $value instanceof \DateTimeInterface) { return $this->formatValue($value, self::PRETTY_DATE); diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php index 010c051581e70..659bd8569759d 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineInitializer.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\Doctrine\Validator; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; use Symfony\Component\Validator\ObjectInitializerInterface; /** diff --git a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php index 76a2b4d8b0288..7ea316f41a2d0 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php +++ b/src/Symfony/Bridge/Doctrine/Validator/DoctrineLoader.php @@ -11,14 +11,17 @@ namespace Symfony\Bridge\Doctrine\Validator; -use Doctrine\Common\Persistence\Mapping\MappingException; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadataInfo; use Doctrine\ORM\Mapping\MappingException as OrmMappingException; +use Doctrine\Persistence\Mapping\MappingException; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; +use Symfony\Component\PropertyInfo\Type; use Symfony\Component\Validator\Constraints\Length; use Symfony\Component\Validator\Constraints\Valid; +use Symfony\Component\Validator\Mapping\AutoMappingStrategy; use Symfony\Component\Validator\Mapping\ClassMetadata; +use Symfony\Component\Validator\Mapping\Loader\AutoMappingTrait; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; /** @@ -28,6 +31,8 @@ */ final class DoctrineLoader implements LoaderInterface { + use AutoMappingTrait; + private $entityManager; private $classValidatorRegexp; @@ -43,13 +48,9 @@ public function __construct(EntityManagerInterface $entityManager, string $class public function loadClassMetadata(ClassMetadata $metadata): bool { $className = $metadata->getClassName(); - if (null === $this->classValidatorRegexp || !preg_match($this->classValidatorRegexp, $className)) { - return false; - } - try { $doctrineMetadata = $this->entityManager->getClassMetadata($className); - } catch (MappingException | OrmMappingException $exception) { + } catch (MappingException|OrmMappingException $exception) { return false; } @@ -57,6 +58,9 @@ public function loadClassMetadata(ClassMetadata $metadata): bool return false; } + $loaded = false; + $enabledForClass = $this->isAutoMappingEnabledForClass($metadata, $this->classValidatorRegexp); + /* Available keys: - type - scale @@ -69,41 +73,52 @@ public function loadClassMetadata(ClassMetadata $metadata): bool // Type and nullable aren't handled here, use the PropertyInfo Loader instead. foreach ($doctrineMetadata->fieldMappings as $mapping) { + $enabledForProperty = $enabledForClass; + $lengthConstraint = null; + foreach ($metadata->getPropertyMetadata($mapping['fieldName']) as $propertyMetadata) { + // Enabling or disabling auto-mapping explicitly always takes precedence + if (AutoMappingStrategy::DISABLED === $propertyMetadata->getAutoMappingStrategy()) { + continue 2; + } + if (AutoMappingStrategy::ENABLED === $propertyMetadata->getAutoMappingStrategy()) { + $enabledForProperty = true; + } + + foreach ($propertyMetadata->getConstraints() as $constraint) { + if ($constraint instanceof Length) { + $lengthConstraint = $constraint; + } + } + } + + if (!$enabledForProperty) { + continue; + } + if (true === ($mapping['unique'] ?? false) && !isset($existingUniqueFields[$mapping['fieldName']])) { $metadata->addConstraint(new UniqueEntity(['fields' => $mapping['fieldName']])); + $loaded = true; } - if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) { + if (null === ($mapping['length'] ?? null) || null !== ($mapping['enumType'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) { continue; } - $constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']); - if (null === $constraint) { - if (isset($mapping['originalClass']) && false === strpos($mapping['declaredField'], '.')) { + if (null === $lengthConstraint) { + if (isset($mapping['originalClass']) && !str_contains($mapping['declaredField'], '.')) { $metadata->addPropertyConstraint($mapping['declaredField'], new Valid()); + $loaded = true; } elseif (property_exists($className, $mapping['fieldName'])) { $metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']])); + $loaded = true; } - } elseif (null === $constraint->max) { + } elseif (null === $lengthConstraint->max) { // If a Length constraint exists and no max length has been explicitly defined, set it - $constraint->max = $mapping['length']; - } - } - - return true; - } - - private function getLengthConstraint(ClassMetadata $metadata, string $fieldName): ?Length - { - foreach ($metadata->getPropertyMetadata($fieldName) as $propertyMetadata) { - foreach ($propertyMetadata->getConstraints() as $constraint) { - if ($constraint instanceof Length) { - return $constraint; - } + $lengthConstraint->max = $mapping['length']; } } - return null; + return $loaded; } private function getExistingUniqueFields(ClassMetadata $metadata): array diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index d022f57c32de2..b4ebc03d319fb 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/doctrine-bridge", "type": "symfony-bridge", - "description": "Symfony Doctrine Bridge", + "description": "Provides integration for Doctrine with various Symfony components", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,40 +16,48 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "doctrine/event-manager": "~1.0", - "doctrine/persistence": "~1.0", + "doctrine/persistence": "^1.3|^2|^3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", - "symfony/service-contracts": "^1.1" + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2" }, "require-dev": { + "composer/package-versions-deprecated": "^1.8", "symfony/stopwatch": "^3.4|^4.0|^5.0", "symfony/config": "^4.2|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", - "symfony/form": "^ 1241 4.3|^5.0", - "symfony/http-kernel": "^3.4|^4.0|^5.0", - "symfony/messenger": "^4.3|^5.0", + "symfony/form": "^4.4.41|^5.0.11", + "symfony/http-kernel": "^4.3.7", + "symfony/messenger": "^4.4|^5.0", "symfony/property-access": "^3.4|^4.0|^5.0", "symfony/property-info": "^3.4|^4.0|^5.0", "symfony/proxy-manager-bridge": "^3.4|^4.0|^5.0", - "symfony/security-core": "^3.4|^4.0|^5.0", + "symfony/security-core": "^4.4|^5.0", "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/validator": "^3.4|^4.0|^5.0", + "symfony/validator": "^4.4.2|^5.0.2", + "symfony/var-dumper": "^3.4|^4.0|^5.0", "symfony/translation": "^3.4|^4.0|^5.0", - "doctrine/annotations": "~1.0", - "doctrine/cache": "~1.6", + "doctrine/annotations": "^1.10.4", "doctrine/collections": "~1.0", - "doctrine/data-fixtures": "1.0.*", - "doctrine/dbal": "~2.4", - "doctrine/orm": "^2.4.5", - "doctrine/reflection": "~1.0" + "doctrine/data-fixtures": "^1.1", + "doctrine/dbal": "^2.7|^3.0", + "doctrine/orm": "^2.6.3" }, "conflict": { + "doctrine/dbal": "<2.7", + "doctrine/orm": "<2.6.3", + "doctrine/lexer": "<1.1", "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/dependency-injection": "<3.4", - "symfony/form": "<4.3", - "symfony/messenger": "<4.3" + "symfony/form": "<4.4", + "symfony/http-kernel": "<4.3.7", + "symfony/messenger": "<4.3", + "symfony/proxy-manager-bridge": "<4.4.19", + "symfony/security-core": "<4.4", + "symfony/validator": "<4.4.2|<5.0.2,>=5.0" }, "suggest": { "symfony/form": "", @@ -65,10 +73,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bridge/Monolog/.gitattributes b/src/Symfony/Bridge/Monolog/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index 20f0dc788b709..61ab0b3c6f899 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -4,7 +4,9 @@ CHANGELOG 4.4.0 ----- -* The `RouteProcessor` class has been made final + * The `RouteProcessor` class has been made final + * Added `ElasticsearchLogstashHandler` + * Added the `ServerLogCommand`. Backport from the deprecated WebServerBundle 4.3.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php new file mode 100644 index 0000000000000..0e5fddc222875 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Command; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; +use Symfony\Bridge\Monolog\Handler\ConsoleHandler; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\LogicException; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\ExpressionLanguage\ExpressionLanguage; + +/** + * @author GrΓ©goire Pineau + */ +class ServerLogCommand extends Command +{ + private const BG_COLOR = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; + + private $el; + private $handler; + + protected static $defaultName = 'server:log'; + + public function isEnabled() + { + if (!class_exists(ConsoleFormatter::class)) { + return false; + } + + // based on a symfony/symfony package, it crashes due a missing FormatterInterface from monolog/monolog + if (!interface_exists(FormatterInterface::class)) { + return false; + } + + return F438 parent::isEnabled(); + } + + protected function configure() + { + if (!class_exists(ConsoleFormatter::class)) { + return; + } + + $this + ->addOption('host', null, InputOption::VALUE_REQUIRED, 'The server host', '0.0.0.0:9911') + ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) + ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) + ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') + ->setDescription('Start a log server that displays logs in real time') + ->setHelp(<<<'EOF' +%command.name% starts a log server to display in real time the log +messages generated by your application: + + php %command.full_name% + +To filter the log messages using any ExpressionLanguage compatible expression, use the --filter option: + +php %command.full_name% --filter="level > 200 or channel in ['app', 'doctrine']" +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $filter = $input->getOption('filter'); + if ($filter) { + if (!class_exists(ExpressionLanguage::class)) { + throw new LogicException('Package "symfony/expression-language" is required to use the "filter" option.'); + } + $this->el = new ExpressionLanguage(); + } + + $this->handler = new ConsoleHandler($output, true, [ + OutputInterface::VERBOSITY_NORMAL => Logger::DEBUG, + ]); + + $this->handler->setFormatter(new ConsoleFormatter([ + 'format' => str_replace('\n', "\n", $input->getOption('format')), + 'date_format' => $input->getOption('date-format'), + 'colors' => $output->isDecorated(), + 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(), + ])); + + if (!str_contains($host = $input->getOption('host'), '://')) { + $host = 'tcp://'.$host; + } + + if (!$socket = stream_socket_server($host, $errno, $errstr)) { + throw new RuntimeException(sprintf('Server start failed on "%s": ', $host).$errstr.' '.$errno); + } + + foreach ($this->getLogs($socket) as $clientId => $message) { + $record = unserialize(base64_decode($message)); + + // Impossible to decode the message, give up. + if (false === $record) { + continue; + } + + if ($filter && !$this->el->evaluate($filter, $record)) { + continue; + } + + $this->displayLog($output, $clientId, $record); + } + + return 0; + } + + private function getLogs($socket): iterable + { + $sockets = [(int) $socket => $socket]; + $write = []; + + while (true) { + $read = $sockets; + stream_select($read, $write, $write, null); + + foreach ($read as $stream) { + if ($socket === $stream) { + $stream = stream_socket_accept($socket); + $sockets[(int) $stream] = $stream; + } elseif (feof($stream)) { + unset($sockets[(int) $stream]); + fclose($stream); + } else { + yield (int) $stream => fgets($stream); + } + } + } + } + + private function displayLog(OutputInterface $output, int $clientId, array $record) + { + if (isset($record['log_id'])) { + $clientId = unpack('H*', $record['log_id'])[1]; + } + $logBlock = sprintf(' ', self::BG_COLOR[$clientId % 8]); + $output->write($logBlock); + + $this->handler->handle($record); + } +} diff --git a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php index aafe9e45e07be..4b87c264e4d5a 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/ConsoleFormatter.php @@ -27,10 +27,10 @@ */ class ConsoleFormatter implements FormatterInterface { - const SIMPLE_FORMAT = "%datetime% %start_tag%%level_name%%end_tag% [%channel%] %message%%context%%extra%\n"; - const SIMPLE_DATE = 'H:i:s'; + public const SIMPLE_FORMAT = "%datetime% %start_tag%%level_name%%end_tag% [%channel%] %message%%context%%extra%\n"; + public const SIMPLE_DATE = 'H:i:s'; - private static $levelColorMap = [ + private const LEVEL_COLOR_MAP = [ Logger::DEBUG => 'fg=white', Logger::INFO => 'fg=green', Logger::NOTICE => 'fg=blue', @@ -70,7 +70,7 @@ public function __construct(array $options = []) '*' => [$this, 'castObject'], ]); - $this->outputBuffer = fopen('php://memory', 'r+b'); + $this->outputBuffer = fopen('php://memory', 'r+'); if ($this->options['multiline']) { $output = $this->outputBuffer; } else { @@ -83,6 +83,8 @@ public function __construct(array $options = []) /** * {@inheritdoc} + * + * @return mixed */ public function formatBatch(array $records) { @@ -95,13 +97,13 @@ public function formatBatch(array $records) /** * {@inheritdoc} + * + * @return mixed */ public function format(array $record) { $record = $this->replacePlaceHolder($record); - $levelColor = self::$levelColorMap[$record['level']]; - if (!$this->options['ignore_empty_context_and_extra'] || !empty($record['context'])) { $context = ($this->options['multiline'] ? "\n" : ' ').$this->dumpData($record['context']); } else { @@ -118,7 +120,7 @@ public function format(array $record) '%datetime%' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->format($this->options['date_format']) : $record['datetime'], - '%start_tag%' => sprintf('<%s>', $levelColor), + '%start_tag%' => sprintf('<%s>', self::LEVEL_COLOR_MAP[$record['level']]), '%level_name%' => sprintf($this->options['level_name_format'], $record['level_name']), '%end_tag%' => '', '%channel%' => $record['channel'], @@ -133,7 +135,7 @@ public function format(array $record) /** * @internal */ - public function echoLine($line, $depth, $indentPad) + public function echoLine(string $line, int $depth, string $indentPad) { if (-1 !== $depth) { fwrite($this->outputBuffer, $line); @@ -143,7 +145,7 @@ public function echoLine($line, $depth, $indentPad) /** * @internal */ - public function castObject($v, array $a, Stub $s, $isNested) + public function castObject($v, array $a, Stub $s, bool $isNested): array { if ($this->options['multiline']) { return $a; @@ -157,11 +159,11 @@ public function castObject($v, array $a, Stub $s, $isNested) return $a; } - private function replacePlaceHolder(array $record) + private function replacePlaceHolder(array $record): array { $message = $record['message']; - if (false === strpos($message, '{')) { + if (!str_contains($message, '{')) { return $record; } @@ -180,7 +182,7 @@ private function replacePlaceHolder(array $record) return $record; } - private function dumpData($data, $colors = null) + private function dumpData($data, bool $colors = null): string { if (null === $this->dumper) { return ''; diff --git a/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php b/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php index e96b510a8bb3f..54988766c3a2d 100644 --- a/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php +++ b/src/Symfony/Bridge/Monolog/Formatter/VarDumperFormatter.php @@ -23,9 +23,14 @@ class VarDumperFormatter implements FormatterInterface public function __construct(VarCloner $cloner = null) { - $this->cloner = $cloner ?: new VarCloner(); + $this->cloner = $cloner ?? new VarCloner(); } + /** + * {@inheritdoc} + * + * @return mixed + */ public function format(array $record) { $record['context'] = $this->cloner->cloneVar($record['context']); @@ -34,6 +39,11 @@ public function format(array $record) return $record; } + /** + * {@inheritdoc} + * + * @return mixed + */ public function formatBatch(array $records) { foreach ($records as $k => $record) { diff --git a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php index 4f98d58b1ffbc..4d722c46ecfcf 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ChromePhpHandler.php @@ -41,7 +41,7 @@ public function onKernelResponse(FilterResponseEvent $event) } if (!preg_match(static::USER_AGENT_REGEX, $event->getRequest()->headers->get('User-Agent'))) { - $this->sendHeaders = false; + self::$sendHeaders = false; $this->headers = []; return; @@ -59,7 +59,7 @@ public function onKernelResponse(FilterResponseEvent $event) */ protected function sendHeader($header, $content) { - if (!$this->sendHeaders) { + if (!self::$sendHeaders) { return; } @@ -72,6 +72,8 @@ protected function sendHeader($header, $content) /** * Override default behavior since we check it in onKernelResponse. + * + * @return bool */ protected function headersAccepted() { diff --git a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php index 1ec91e43f29a2..2a02905d24e21 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ConsoleHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Handler; +use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; use Monolog\Handler\AbstractProcessingHandler; use Monolog\Logger; @@ -50,7 +51,7 @@ class ConsoleHandler extends AbstractProcessingHandler implements EventSubscribe OutputInterface::VERBOSITY_VERY_VERBOSE => Logger::INFO, OutputInterface::VERBOSITY_DEBUG => Logger::DEBUG, ]; - private $consoleFormaterOptions; + private $consoleFormatterOptions; /** * @param OutputInterface|null $output The console output to use (the handler remains disabled when passing null @@ -59,7 +60,7 @@ class ConsoleHandler extends AbstractProcessingHandler implements EventSubscribe * @param array $verbosityLevelMap Array that maps the OutputInterface verbosity to a minimum logging * level (leave empty to use the default mapping) */ - public function __construct(OutputInterface $output = null, bool $bubble = true, array $verbosityLevelMap = [], array $consoleFormaterOptions = []) + public function __construct(OutputInterface $output = null, bool $bubble = true, array $verbosityLevelMap = [], array $consoleFormatterOptions = []) { parent::__construct(Logger::DEBUG, $bubble); $this->output = $output; @@ -68,11 +69,13 @@ public function __construct(OutputInterface $output = null, bool $bubble = true, $this->verbosityLevelMap = $verbosityLevelMap; } - $this->consoleFormaterOptions = $consoleFormaterOptions; + $this->consoleFormatterOptions = $consoleFormatterOptions; } /** * {@inheritdoc} + * + * @return bool */ public function isHandling(array $record) { @@ -81,6 +84,8 @@ public function isHandling(array $record) /** * {@inheritdoc} + * + * @return bool */ public function handle(array $record) { @@ -142,6 +147,8 @@ public static function getSubscribedEvents() /** * {@inheritdoc} + * + * @return void */ protected function write(array $record) { @@ -151,6 +158,8 @@ protected function write(array $record) /** * {@inheritdoc} + * + * @return FormatterInterface */ protected function getDefaultFormatter() { @@ -158,13 +167,13 @@ protected function getDefaultFormatter() return new LineFormatter(); } if (!$this->output) { - return new ConsoleFormatter($this->consoleFormaterOptions); + return new ConsoleFormatter($this->consoleFormatterOptions); } return new ConsoleFormatter(array_replace([ 'colors' => $this->output->isDecorated(), 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $this->output->getVerbosity(), - ], $this->consoleFormaterOptions)); + ], $this->consoleFormatterOptions)); } /** @@ -172,7 +181,7 @@ protected function getDefaultFormatter() * * @return bool Whether the handler is enabled and verbosity is not set to quiet */ - private function updateLevel() + private function updateLevel(): bool { if (null === $this->output) { return false; diff --git a/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.php new file mode 100644 index 0000000000000..9826bb525773c --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/ElasticsearchLogstashHandler.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\Bridge\Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogstashFormatter; +use Monolog\Handler\AbstractHandler; +use Monolog\Handler\FormattableHandlerTrait; +use Monolog\Handler\ProcessableHandlerTrait; +use Monolog\Logger; +use Symfony\Component\HttpClient\HttpClient; +use Symfony\Contracts\HttpClient\Exception\ExceptionInterface; +use Symfony\Contracts\HttpClient\HttpClientInterface; + +/** + * Push logs directly to Elasticsearch and format them according to Logstash specification. + * + * This handler dials directly with the HTTP interface of Elasticsearch. This + * means it will slow down your application if Elasticsearch takes times to + * answer. Even if all HTTP calls are done asynchronously. + * + * In a development environment, it's fine to keep the default configuration: + * for each log, an HTTP request will be made to push the log to Elasticsearch. + * + * In a production environment, it's highly recommended to wrap this handler + * in a handler with buffering capabilities (like the FingersCrossedHandler, or + * BufferHandler) in order to call Elasticsearch only once with a bulk push. For + * even better performance and fault tolerance, a proper ELK (https://www.elastic.co/what-is/elk-stack) + * stack is recommended. + * + * @author GrΓ©goire Pineau + */ +class ElasticsearchLogstashHandler extends AbstractHandler +{ + use FormattableHandlerTrait; + use ProcessableHandlerTrait; + + private $endpoint; + private $index; + private $client; + private $responses; + private $elasticsearchVersion; + + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + */ + public function __construct(string $endpoint = 'http://127.0.0.1:9200', string $index = 'monolog', HttpClientInterface $client = null, $level = Logger::DEBUG, bool $bubble = true, string $elasticsearchVersion = '1.0.0') + { + if (!interface_exists(HttpClientInterface::class)) { + throw new \LogicException(sprintf('The "%s" handler needs an HTTP client. Try running "composer require symfony/http-client".', __CLASS__)); + } + + parent::__construct($level, $bubble); + $this->endpoint = $endpoint; + $this->index = $index; + $this->client = $client ?: HttpClient::create(['timeout' => 1]); + $this->responses = new \SplObjectStorage(); + $this->elasticsearchVersion = $elasticsearchVersion; + } + + public function handle(array $record): bool + { + if (!$this->isHandling($record)) { + return false; + } + + $this->sendToElasticsearch([$record]); + + return !$this->bubble; + } + + public function handleBatch(array $records): void + { + $records = array_filter($records, [$this, 'isHandling']); + + if ($records) { + $this->sendToElasticsearch($records); + } + } + + protected function getDefaultFormatter(): FormatterInterface + { + // Monolog 1.X + if (\defined(LogstashFormatter::class.'::V1')) { + return new LogstashFormatter('application', null, null, 'ctxt_', LogstashFormatter::V1); + } + + // Monolog 2.X + return new LogstashFormatter('application'); + } + + private function sendToElasticsearch(array $records) + { + $formatter = $this->getFormatter(); + + if (version_compare($this->elasticsearchVersion, '7', '>=')) { + $headers = json_encode([ + 'index' => [ + '_index' => $this->index, + ], + ]); + } else { + $headers = json_encode([ + 'index' => [ + '_index' => $this->index, + '_type' => '_doc', + ], + ]); + } + + $body = ''; + foreach ($records as $record) { + foreach ($this->processors as $processor) { + $record = $processor($record); + } + + $body .= $headers; + $body .= "\n"; + $body .= $formatter->format($record); + $body .= "\n"; + } + + $response = $this->client->request('POST', $this->endpoint.'/_bulk', [ + 'body' => $body, + 'headers' => [ + 'Content-Type' => 'application/json', + ], + ]); + + $this->responses->attach($response); + + $this->wait(false); + } + + /** + * @return array + */ + public function __sleep() + { + throw new \BadMethodCallException('Cannot serialize '.__CLASS__); + } + + public function __wakeup() + { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + + public function __destruct() + { + $this->wait(true); + } + + private function wait(bool $blocking) + { + foreach ($this->client->stream($this->responses, $blocking ? null : 0.0) as $response => $chunk) { + try { + if ($chunk->isTimeout() && !$blocking) { + continue; + } + if (!$chunk->isFirst() && !$chunk->isLast()) { + continue; + } + if ($chunk->isLast()) { + $this->responses->detach($response); + } + } catch (ExceptionInterface $e) { + $this->responses->detach($response); + error_log(sprintf("Could not push logs to Elasticsearch:\n%s", (string) $e)); + } + } + } +} diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php index ae8fd3650a36b..84f61ce9bf706 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/HttpCodeActivationStrategy.php @@ -32,10 +32,10 @@ public function __construct(RequestStack $requestStack, array $exclusions, $acti { foreach ($exclusions as $exclusion) { if (!\array_key_exists('code', $exclusion)) { - throw new \LogicException(sprintf('An exclusion must have a "code" key')); + throw new \LogicException('An exclusion must have a "code" key.'); } if (!\array_key_exists('urls', $exclusion)) { - throw new \LogicException(sprintf('An exclusion must have a "urls" key')); + throw new \LogicException('An exclusion must have a "urls" key.'); } } @@ -45,6 +45,9 @@ public function __construct(RequestStack $requestStack, array $exclusions, $acti $this->exclusions = $exclusions; } + /** + * @return bool + */ public function isHandlerActivated(array $record) { $isActivated = parent::isHandlerActivated($record); diff --git a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php index ed41929a2cef3..a404f39db3cab 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php +++ b/src/Symfony/Bridge/Monolog/Handler/FingersCrossed/NotFoundActivationStrategy.php @@ -23,7 +23,7 @@ */ class NotFoundActivationStrategy extends ErrorLevelActivationStrategy { - private $blacklist; + private $exclude; private $requestStack; public function __construct(RequestStack $requestStack, array $excludedUrls, $actionLevel) @@ -31,9 +31,12 @@ public function __construct(RequestStack $requestStack, array $excludedUrls, $ac parent::__construct($actionLevel); $this->requestStack = $requestStack; - $this->blacklist = '{('.implode('|', $excludedUrls).')}i'; + $this->exclude = '{('.implode('|', $excludedUrls).')}i'; } + /** + * @return bool + */ public function isHandlerActivated(array $record) { $isActivated = parent::isHandlerActivated($record); @@ -45,7 +48,7 @@ public function isHandlerActivated(array $record) && 404 == $record['context']['exception']->getStatusCode() && ($request = $this->requestStack->getMasterRequest()) ) { - return !preg_match($this->blacklist, $request->getPathInfo()); + return !preg_match($this->exclude, $request->getPathInfo()); } return $isActivated; diff --git a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php index b235fc101ea73..f006118223cba 100644 --- a/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/FirePHPHandler.php @@ -74,6 +74,8 @@ protected function sendHeader($header, $content) /** * Override default behavior since we check the user agent in onKernelResponse. + * + * @return bool */ protected function headersAccepted() { diff --git a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php index 22c035731dc5b..8101178d66c03 100644 --- a/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/ServerLogHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Monolog\Handler; +use Monolog\Formatter\FormatterInterface; use Monolog\Handler\AbstractHandler; use Monolog\Logger; use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter; @@ -24,11 +25,14 @@ class ServerLogHandler extends AbstractHandler private $context; private $socket; - public function __construct(string $host, int $level = Logger::DEBUG, bool $bubble = true, array $context = []) + /** + * @param string|int $level The minimum logging level at which this handler will be triggered + */ + public function __construct(string $host, $level = Logger::DEBUG, bool $bubble = true, array $context = []) { parent::__construct($level, $bubble); - if (false === strpos($host, '://')) { + if (!str_contains($host, '://')) { $host = 'tcp://'.$host; } @@ -38,6 +42,8 @@ public function __construct(string $host, int $level = Logger::DEBUG, bool $bubb /** * {@inheritdoc} + * + * @return bool */ public function handle(array $record) { @@ -61,7 +67,7 @@ public function handle(array $record) try { if (-1 === stream_socket_sendto($this->socket, $recordFormatted)) { - stream_socket_shutdown($this->socket, STREAM_SHUT_RDWR); + stream_socket_shutdown($this->socket, \STREAM_SHUT_RDWR); // Let's retry: the persistent connection might just be stale if ($this->socket = $this->createSocket()) { @@ -77,6 +83,8 @@ public function handle(array $record) /** * {@inheritdoc} + * + * @return FormatterInterface */ protected function getDefaultFormatter() { @@ -89,7 +97,7 @@ private static function nullErrorHandler() private function createSocket() { - $socket = stream_socket_client($this->host, $errno, $errstr, 0, STREAM_CLIENT_CONNECT | STREAM_CLIENT_ASYNC_CONNECT | STREAM_CLIENT_PERSISTENT, $this->context); + $socket = stream_socket_client($this->host, $errno, $errstr, 0, \STREAM_CLIENT_CONNECT | \STREAM_CLIENT_ASYNC_CONNECT | \STREAM_CLIENT_PERSISTENT, $this->context); if ($socket) { stream_set_blocking($socket, false); @@ -98,7 +106,7 @@ private function createSocket() return $socket; } - private function formatRecord(array $record) + private function formatRecord(array $record): string { if ($this->processors) { foreach ($this->processors as $processor) { diff --git a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php index 93f2f72e6457a..1143c0668093c 100644 --- a/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php +++ b/src/Symfony/Bridge/Monolog/Handler/SwiftMailerHandler.php @@ -85,7 +85,7 @@ private function flushMemorySpool() } if (null === $this->transport) { - throw new \Exception('No transport available to flush mail queue'); + throw new \Exception('No transport available to flush mail queue.'); } $spool->flushQueue($this->transport); diff --git a/src/Symfony/Bridge/Monolog/LICENSE b/src/Symfony/Bridge/Monolog/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bridge/Monolog/LICENSE +++ b/src/Symfony/Bridge/Monolog/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/Monolog/Logger.php b/src/Symfony/Bridge/Monolog/Logger.php index 5141ac955f44d..336d2e7f0dc33 100644 --- a/src/Symfony/Bridge/Monolog/Logger.php +++ b/src/Symfony/Bridge/Monolog/Logger.php @@ -29,8 +29,8 @@ class Logger extends BaseLogger implements DebugLoggerInterface, ResetInterface */ public function getLogs(/* Request $request = null */) { - if (\func_num_args() < 1 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); } if ($logger = $this->getDebugLogger()) { @@ -47,8 +47,8 @@ public function getLogs(/* Request $request = null */) */ public function countErrors(/* Request $request = null */) { - if (\func_num_args() < 1 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); } if ($logger = $this->getDebugLogger()) { @@ -97,10 +97,8 @@ public function removeDebugLogger() /** * Returns a DebugLoggerInterface instance if one is registered with this logger. - * - * @return DebugLoggerInterface|null A DebugLoggerInterface instance or null if none is registered */ - private function getDebugLogger() + private function getDebugLogger(): ?DebugLoggerInterface { foreach ($this->processors as $processor) { if ($processor instanceof DebugLoggerInterface) { @@ -113,5 +111,7 @@ private function getDebugLogger() return $handler; } } + + return null; } } diff --git a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php index f4e37ff44e739..11f075fe3a26e 100644 --- a/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php +++ b/src/Symfony/Bridge/Monolog/Processor/DebugProcessor.php @@ -33,12 +33,12 @@ public function __invoke(array $record) $hash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : ''; $this->records[$hash][] = [ - 'timestamp' => $record['datetime']->getTimestamp(), + 'timestamp' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->getTimestamp() : strtotime($record['datetime']), 'message' => $record['message'], 'priority' => $record['level'], 'priorityName' => $record['level_name'], 'context' => $record['context'], - 'channel' => isset($record['channel']) ? $record['channel'] : '', + 'channel' => $record['channel'] ?? '', ]; if (!isset($this->errorCount[$hash])) { @@ -63,8 +63,8 @@ public function __invoke(array $record) */ public function getLogs(/* Request $request = null */) { - if (\func_num_args() < 1 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); } if (1 <= \func_num_args() && null !== $request = func_get_arg(0)) { @@ -85,8 +85,8 @@ public function getLogs(/* Request $request = null */) */ public function countErrors(/* Request $request = null */) { - if (\func_num_args() < 1 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), E_USER_DEPRECATED); + if (\func_num_args() < 1 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', __METHOD__), \E_USER_DEPRECATED); } if (1 <= \func_num_args() && null !== $request = func_get_arg(0)) { diff --git a/src/Symfony/Bridge/Monolog/README.md b/src/Symfony/Bridge/Monolog/README.md index 2d19b3e27cfd4..112c05c629e50 100644 --- a/src/Symfony/Bridge/Monolog/README.md +++ b/src/Symfony/Bridge/Monolog/README.md @@ -1,12 +1,13 @@ Monolog Bridge ============== -Provides integration for Monolog with various Symfony components. +The Monolog bridge provides integration for +[Monolog](https://seldaek.github.io/monolog/) with various Symfony components. Resources --------- - * [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) + * [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/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php b/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.php new file mode 100644 index 0000000000000..31c62e3e75591 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/ClassThatInheritLogger.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\Bridge\Monolog\Tests; + +use Symfony\Bridge\Monolog\Logger; + +class ClassThatInheritLogger extends Logger +{ + public function getLogs(): array + { + return parent::getLogs(); + } + + public function countErrors(): int + { + return parent::countErrors(); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Formatter/ConsoleFormatterTest.php b/src/Symfony/Bridge/Monolog/Tests/Formatter/ConsoleFormatterTest.php index c09597e916cfc..89d5bee454548 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Formatter/ConsoleFormatterTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Formatter/ConsoleFormatterTest.php @@ -26,10 +26,7 @@ public function testFormat(array $record, $expectedMessage) self::assertSame($expectedMessage, $formatter->format($record)); } - /** - * @return array - */ - public function providerFormatTests() + public function providerFormatTests(): array { $currentDateTime = new \DateTime(); diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php index afd07683e43d5..d61692ed76466 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ConsoleHandlerTest.php @@ -13,12 +13,15 @@ use Monolog\Logger; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Formatter\ConsoleFormatter; use Symfony\Bridge\Monolog\Handler\ConsoleHandler; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\ConsoleEvents; use Symfony\Component\Console\Event\ConsoleCommandEvent; use Symfony\Component\Console\Event\ConsoleTerminateEvent; +use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\Output; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -46,7 +49,7 @@ public function testIsHandling() */ public function testVerbosityMapping($verbosity, $level, $isHandling, array $map = []) { - $output = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); + $output = $this->createMock(OutputInterface::class); $output ->expects($this->atLeastOnce()) ->method('getVerbosity') @@ -61,7 +64,7 @@ public function testVerbosityMapping($verbosity, $level, $isHandling, array $map $levelName = Logger::getLevelName($level); $levelName = sprintf('%-9s', $levelName); - $realOutput = $this->getMockBuilder('Symfony\Component\Console\Output\Output')->setMethods(['doWrite'])->getMock(); + $realOutput = $this->getMockBuilder(Output::class)->setMethods(['doWrite'])->getMock(); $realOutput->setVerbosity($verbosity); if ($realOutput->isDebug()) { $log = "16:21:54 $levelName [app] My info message\n"; @@ -110,16 +113,14 @@ public function provideVerbosityMappingTests() public function testVerbosityChanged() { - $output = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); + $output = $this->createMock(OutputInterface::class); $output - ->expects($this->at(0)) + ->expects($this->exactly(2)) ->method('getVerbosity') - ->willReturn(OutputInterface::VERBOSITY_QUIET) - ; - $output - ->expects($this->at(1)) - ->method('getVerbosity') - ->willReturn(OutputInterface::VERBOSITY_DEBUG) + ->willReturnOnConsecutiveCalls( + OutputInterface::VERBOSITY_QUIET, + OutputInterface::VERBOSITY_DEBUG + ) ; $handler = new ConsoleHandler($output); $this->assertFalse($handler->isHandling(['level' => Logger::NOTICE]), @@ -133,14 +134,15 @@ public function testVerbosityChanged() public function testGetFormatter() { $handler = new ConsoleHandler(); - $this->assertInstanceOf('Symfony\Bridge\Monolog\Formatter\ConsoleFormatter', $handler->getFormatter(), - '-getFormatter returns ConsoleFormatter by default' + $this->assertInstanceOf( + ConsoleFormatter::class, $handler->getFormatter(), + '->getFormatter returns ConsoleFormatter by default' ); } public function testWritingAndFormatting() { - $output = $this->getMockBuilder('Symfony\Component\Console\Output\OutputInterface')->getMock(); + $output = $this->createMock(OutputInterface::class); $output ->expects($this->any()) ->method('getVerbosity') @@ -195,14 +197,14 @@ public function testLogsFromListeners() $logger->info('After terminate message.'); }); - $event = new ConsoleCommandEvent(new Command('foo'), $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(), $output); + $event = new ConsoleCommandEvent(new Command('foo'), $this->createMock(InputInterface::class), $output); $dispatcher->dispatch($event, ConsoleEvents::COMMAND); - $this->assertContains('Before command message.', $out = $output->fetch()); - $this->assertContains('After command message.', $out); + $this->assertStringContainsString('Before command message.', $out = $output->fetch()); + $this->assertStringContainsString('After command message.', $out); - $event = new ConsoleTerminateEvent(new Command('foo'), $this->getMockBuilder('Symfony\Component\Console\Input\InputInterface')->getMock(), $output, 0); + $event = new ConsoleTerminateEvent(new Command('foo'), $this->createMock(InputInterface::class), $output, 0); $dispatcher->dispatch($event, ConsoleEvents::TERMINATE); - $this->assertContains('Before terminate message.', $out = $output->fetch()); - $this->assertContains('After terminate message.', $out); + $this->assertStringContainsString('Before terminate message.', $out = $output->fetch()); + $this->assertStringContainsString('After terminate message.', $out); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ElasticsearchLogstashHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ElasticsearchLogstashHandlerTest.php new file mode 100644 index 0000000000000..0a30fb3c63bc6 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ElasticsearchLogstashHandlerTest.php @@ -0,0 +1,180 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\LogstashFormatter; +use Monolog\Logger; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Handler\ElasticsearchLogstashHandler; +use Symfony\Component\HttpClient\MockHttpClient; +use Symfony\Component\HttpClient\Response\MockResponse; + +class ElasticsearchLogstashHandlerTest extends TestCase +{ + public function testHandle() + { + $callCount = 0; + $responseFactory = function ($method, $url, $options) use (&$callCount) { + $body = <<assertSame('POST', $method); + $this->assertSame('http://es:9200/_bulk', $url); + $this->assertSame($body, $options['body']); + $this->assertSame('Content-Type: application/json', $options['normalized_headers']['content-type'][0]); + ++$callCount; + + return new MockResponse(); + }; + + $handler = new ElasticsearchLogstashHandlerWithHardCodedHostname('http://es:9200', 'log', new MockHttpClient($responseFactory)); + + $record = [ + 'message' => 'My info message', + 'context' => [], + 'level' => Logger::INFO, + 'level_name' => Logger::getLevelName(Logger::INFO), + 'channel' => 'app', + 'datetime' => new \DateTime('2020-01-01T00:00:00+01:00'), + 'extra' => [], + ]; + + $handler->handle($record); + + $this->assertSame(1, $callCount); + } + + public function testHandleWithElasticsearch8() + { + $callCount = 0; + $responseFactory = function ($method, $url, $options) use (&$callCount) { + $body = <<assertSame('POST', $method); + $this->assertSame('http://es:9200/_bulk', $url); + $this->assertSame($body, $options['body']); + $this->assertSame('Content-Type: application/json', $options['normalized_headers']['content-type'][0]); + ++$callCount; + + return new MockResponse(); + }; + + $handler = new ElasticsearchLogstashHandlerWithHardCodedHostname('http://es:9200', 'log', new MockHttpClient($responseFactory), Logger::DEBUG, true, '8.0.0'); + + $record = [ + 'message' => 'My info message', + 'context' => [], + 'level' => Logger::INFO, + 'level_name' => Logger::getLevelName(Logger::INFO), + 'channel' => 'app', + 'datetime' => new \DateTime('2020-01-01T00:00:00+01:00'), + 'extra' => [], + ]; + + $handler->handle($record); + + $this->assertSame(1, $callCount); + } + + public function testBandleBatch() + { + $callCount = 0; + $responseFactory = function ($method, $url, $options) use (&$callCount) { + $body = <<assertSame('POST', $method); + $this->assertSame('http://es:9200/_bulk', $url); + $this->assertSame($body, $options['body']); + $this->assertSame('Content-Type: application/json', $options['normalized_headers']['content-type'][0]); + ++$callCount; + + return new MockResponse(); + }; + + $handler = new ElasticsearchLogstashHandlerWithHardCodedHostname('http://es:9200', 'log', new MockHttpClient($responseFactory)); + + $records = [ + [ + 'message' => 'My info message', + 'context' => [], + 'level' => Logger::INFO, + 'level_name' => Logger::getLevelName(Logger::INFO), + 'channel' => 'app', + 'datetime' => new \DateTime('2020-01-01T00:00:00+01:00'), + 'extra' => [], + ], + [ + 'message' => 'My second message', + 'context' => [], + 'level' => Logger::WARNING, + 'level_name' => Logger::getLevelName(Logger::WARNING), + 'channel' => 'php', + 'datetime' => new \DateTime('2020-01-01T00:00:01+01:00'), + 'extra' => [], + ], + ]; + + $handler->handleBatch($records); + + $this->assertSame(1, $callCount); + } +} + +class ElasticsearchLogstashHandlerWithHardCodedHostname extends ElasticsearchLogstashHandler +{ + protected function getDefaultFormatter(): FormatterInterface + { + // Monolog 1.X + if (\defined(LogstashFormatter::class.'::V1')) { + return new LogstashFormatter('application', 'my hostname', null, 'ctxt_', LogstashFormatter::V1); + } + + // Monolog 2.X + return new LogstashFormatter('application', 'my hostname'); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php index 0fce18d3861be..3d84cb3552c0d 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/FingersCrossed/HttpCodeActivationStrategyTest.php @@ -20,19 +20,15 @@ class HttpCodeActivationStrategyTest extends TestCase { - /** - * @expectedException \LogicException - */ public function testExclusionsWithoutCode() { + $this->expectException(\LogicException::class); new HttpCodeActivationStrategy(new RequestStack(), [['urls' => []]], Logger::WARNING); } - /** - * @expectedException \LogicException - */ public function testExclusionsWithoutUrls() { + $this->expectException(\LogicException::class); new HttpCodeActivationStrategy(new RequestStack(), [['code' => 404]], Logger::WARNING); } diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/ServerLogHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/ServerLogHandlerTest.php new file mode 100644 index 0000000000000..f5a4405f645f1 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/ServerLogHandlerTest.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\Bridge\Monolog\Tests\Handler; + +use Monolog\Formatter\JsonFormatter; +use Monolog\Logger; +use Monolog\Processor\ProcessIdProcessor; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Formatter\VarDumperFormatter; +use Symfony\Bridge\Monolog\Handler\ServerLogHandler; +use Symfony\Component\VarDumper\Cloner\Data; + +/** + * Tests the ServerLogHandler. + */ +class ServerLogHandlerTest extends TestCase +{ + public function testFormatter() + { + $handler = new ServerLogHandler('tcp://127.0.0.1:9999'); + $this->assertInstanceOf(VarDumperFormatter::class, $handler->getFormatter()); + + $formatter = new JsonFormatter(); + $handler->setFormatter($formatter); + $this->assertSame($formatter, $handler->getFormatter()); + } + + public function testIsHandling() + { + $handler = new ServerLogHandler('tcp://127.0.0.1:9999', Logger::INFO); + $this->assertFalse($handler->isHandling(['level' => Logger::DEBUG]), '->isHandling returns false when no output is set'); + } + + public function testGetFormatter() + { + $handler = new ServerLogHandler('tcp://127.0.0.1:9999'); + $this->assertInstanceOf(VarDumperFormatter::class, $handler->getFormatter(), + '->getFormatter returns VarDumperFormatter by default' + ); + } + + public function testWritingAndFormatting() + { + $host = 'tcp://127.0.0.1:9999'; + $handler = new ServerLogHandler($host, Logger::INFO, false); + $handler->pushProcessor(new ProcessIdProcessor()); + + $infoRecord = [ + 'message' => 'My info message', + 'context' => [], + 'level' => Logger::INFO, + 'level_name' => Logger::getLevelName(Logger::INFO), + 'channel' => 'app', + 'datetime' => new \DateTime('2013-05-29 16:21:54'), + 'extra' => [], + ]; + + $socket = stream_socket_server($host, $errno, $errstr); + $this->assertIsResource($socket, sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno)); + + $this->assertTrue($handler->handle($infoRecord), 'The handler finished handling the log as bubble is false.'); + + $sockets = [(int) $socket => $socket]; + $write = []; + + for ($i = 0; $i < 10; ++$i) { + $read = $sockets; + stream_select($read, $write, $write, null); + + foreach ($read as $stream) { + if ($socket === $stream) { + $stream = stream_socket_accept($socket); + $sockets[(int) $stream] = $stream; + } elseif (feof($stream)) { + unset($sockets[(int) $stream]); + fclose($stream); + } else { + $message = fgets($stream); + fclose($stream); + + $record = unserialize(base64_decode($message)); + $this->assertIsArray($record); + + $this->assertArrayHasKey('message', $record); + $this->assertEquals('My info message', $record['message']); + + $this->assertArrayHasKey('extra', $record); + $this->assertInstanceOf(Data::class, $record['extra']); + $extra = $record['extra']->getValue(true); + $this->assertIsArray($extra); + $this->assertArrayHasKey('process_id', $extra); + $this->assertSame(getmypid(), $extra['process_id']); + + return; + } + } + usleep(100000); + } + $this->fail('Fail to read message from server'); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php b/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php index 143200f6c5718..12b687e115cdb 100644 --- a/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/LoggerTest.php @@ -75,7 +75,7 @@ public function testGetLogsWithDebugProcessor2() $logger->info('test'); $this->assertCount(1, $logger->getLogs()); - list($record) = $logger->getLogs(); + [$record] = $logger->getLogs(); $this->assertEquals('test', $record['message']); $this->assertEquals(Logger::INFO, $record['priority']); @@ -84,7 +84,7 @@ public function testGetLogsWithDebugProcessor2() public function testGetLogsWithDebugProcessor3() { $request = new Request(); - $processor = $this->getMockBuilder(DebugProcessor::class)->getMock(); + $processor = $this->createMock(DebugProcessor::class); $processor->expects($this->once())->method('getLogs')->with($request); $processor->expects($this->once())->method('countErrors')->with($request); @@ -128,33 +128,12 @@ public function testReset() /** * @group legacy * @expectedDeprecation The "Symfony\Bridge\Monolog\Logger::getLogs()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - */ - public function testInheritedClassCallGetLogsWithoutArgument() - { - $loggerChild = new ClassThatInheritLogger('test'); - $loggerChild->getLogs(); - } - - /** - * @group legacy * @expectedDeprecation The "Symfony\Bridge\Monolog\Logger::countErrors()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. */ - public function testInheritedClassCallCountErrorsWithoutArgument() + public function testInheritedClassWithoutArgument() { $loggerChild = new ClassThatInheritLogger('test'); + $loggerChild->getLogs(); $loggerChild->countErrors(); } } - -class ClassThatInheritLogger extends Logger -{ - public function getLogs() - { - parent::getLogs(); - } - - public function countErrors() - { - parent::countErrors(); - } -} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php b/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.php new file mode 100644 index 0000000000000..1f15bd9f764b2 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/ClassThatInheritDebugProcessor.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\Bridge\Monolog\Tests\Processor; + +use Symfony\Bridge\Monolog\Processor\DebugProcessor; + +class ClassThatInheritDebugProcessor extends DebugProcessor +{ + public function getLogs(): array + { + return parent::getLogs(); + } + + public function countErrors(): int + { + return parent::countErrors(); + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php index 4c9774b4a4385..424f9ce10d597 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/ConsoleCommandProcessorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Event\ConsoleEvent; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; class ConsoleCommandProcessorTest extends TestCase { @@ -61,15 +62,12 @@ public function testProcessorDoesNothingWhenNotInConsole() private function getConsoleEvent(): ConsoleEvent { - $input = $this->getMockBuilder(InputInterface::class)->getMock(); + $input = $this->createMock(InputInterface::class); $input->method('getArguments')->willReturn(self::TEST_ARGUMENTS); $input->method('getOptions')->willReturn(self::TEST_OPTIONS); - $command = $this->getMockBuilder(Command::class)->disableOriginalConstructor()->getMock(); + $command = $this->createMock(Command::class); $command->method('getName')->willReturn(self::TEST_NAME); - $consoleEvent = $this->getMockBuilder(ConsoleEvent::class)->disableOriginalConstructor()->getMock(); - $consoleEvent->method('getCommand')->willReturn($command); - $consoleEvent->method('getInput')->willReturn($input); - return $consoleEvent; + return new ConsoleEvent($command, $input, $this->createMock(OutputInterface::class)); } } diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php index 9aceb9337e403..4ac41c978ec4e 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/DebugProcessorTest.php @@ -19,6 +19,30 @@ class DebugProcessorTest extends TestCase { + /** + * @dataProvider providerDatetimeFormatTests + */ + public function testDatetimeFormat(array $record, $expectedTimestamp) + { + $processor = new DebugProcessor(); + $processor($record); + + $records = $processor->getLogs(); + self::assertCount(1, $records); + self::assertSame($expectedTimestamp, $records[0]['timestamp']); + } + + public function providerDatetimeFormatTests(): array + { + $record = $this->getRecord(); + + return [ + [array_merge($record, ['datetime' => new \DateTime('2019-01-01T00:01:00+00:00')]), 1546300860], + [array_merge($record, ['datetime' => '2019-01-01T00:01:00+00:00']), 1546300860], + [array_merge($record, ['datetime' => 'foo']), false], + ]; + } + public function testDebugProcessor() { $processor = new DebugProcessor(); @@ -66,24 +90,16 @@ public function testWithRequestStack() /** * @group legacy * @expectedDeprecation The "Symfony\Bridge\Monolog\Processor\DebugProcessor::getLogs()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. - */ - public function testInheritedClassCallGetLogsWithoutArgument() - { - $debugProcessorChild = new ClassThatInheritDebugProcessor(); - $debugProcessorChild->getLogs(); - } - - /** - * @group legacy * @expectedDeprecation The "Symfony\Bridge\Monolog\Processor\DebugProcessor::countErrors()" method will have a new "Request $request = null" argument in version 5.0, not defining it is deprecated since Symfony 4.2. */ - public function testInheritedClassCallCountErrorsWithoutArgument() + public function testInheritedClassWithoutArgument() { $debugProcessorChild = new ClassThatInheritDebugProcessor(); + $debugProcessorChild->getLogs(); $debugProcessorChild->countErrors(); } - private function getRecord($level = Logger::WARNING, $message = 'test') + private function getRecord($level = Logger::WARNING, $message = 'test'): array { return [ 'message' => $message, @@ -96,16 +112,3 @@ private function getRecord($level = Logger::WARNING, $message = 'test') ]; } } - -class ClassThatInheritDebugProcessor extends DebugProcessor -{ - public function getLogs() - { - parent::getLogs(); - } - - 10000 public function countErrors() - { - parent::countErrors(); - } -} diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php index 5534240ba278a..06336f1a593ca 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/RouteProcessorTest.php @@ -16,7 +16,8 @@ use Symfony\Component\HttpFoundation\ParameterBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\FinishRequestEvent; -use Symfony\Component\HttpKernel\Event\GetResponseEvent; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; class RouteProcessorTest extends TestCase { @@ -28,7 +29,7 @@ public function testProcessor() { $request = $this->mockFilledRequest(); $processor = new RouteProcessor(); - $processor->addRouteData($this->mockGetResponseEvent($request)); + $processor->addRouteData($this->getRequestEvent($request)); $record = $processor(['extra' => []]); @@ -44,7 +45,7 @@ public function testProcessorWithoutParams() { $request = $this->mockFilledRequest(); $processor = new RouteProcessor(false); - $processor->addRouteData($this->mockGetResponseEvent($request)); + $processor->addRouteData($this->getRequestEvent($request)); $record = $processor(['extra' => []]); @@ -63,8 +64,8 @@ public function testProcessorWithSubRequests() $subRequest = $this->mockFilledRequest($controllerFromSubRequest); $processor = new RouteProcessor(false); - $processor->addRouteData($this->mockGetResponseEvent($mainRequest)); - $processor->addRouteData($this->mockGetResponseEvent($subRequest)); + $processor->addRouteData($this->getRequestEvent($mainRequest)); + $processor->addRouteData($this->getRequestEvent($subRequest, HttpKernelInterface::SUB_REQUEST)); $record = $processor(['extra' => []]); @@ -86,9 +87,9 @@ public function testFinishRequestRemovesRelatedEntry() $subRequest = $this->mockFilledRequest('OtherController::otherMethod'); $processor = new RouteProcessor(false); - $processor->addRouteData($this->mockGetResponseEvent($mainRequest)); - $processor->addRouteData($this->mockGetResponseEvent($subRequest)); - $processor->removeRouteData($this->mockFinishRequestEvent($subRequest)); + $processor->addRouteData($this->getRequestEvent($mainRequest)); + $processor->addRouteData($this->getRequestEvent($subRequest, HttpKernelInterface::SUB_REQUEST)); + $processor->removeRouteData($this->getFinishRequestEvent($subRequest)); $record = $processor(['extra' => []]); $this->assertArrayHasKey('requests', $record['extra']); @@ -98,7 +99,7 @@ public function testFinishRequestRemovesRelatedEntry() $record['extra']['requests'][0] ); - $processor->removeRouteData($this->mockFinishRequestEvent($mainRequest)); + $processor->removeRouteData($this->getFinishRequestEvent($mainRequest)); $record = $processor(['extra' => []]); $this->assertArrayNotHasKey('requests', $record['extra']); @@ -108,7 +109,7 @@ public function testProcessorWithEmptyRequest() { $request = $this->mockEmptyRequest(); $processor = new RouteProcessor(); - $processor->addRouteData($this->mockGetResponseEvent($request)); + $processor->addRouteData($this->getRequestEvent($request)); $record = $processor(['extra' => []]); $this->assertEquals(['extra' => []], $record); @@ -122,20 +123,14 @@ public function testProcessorDoesNothingWhenNoRequest() $this->assertEquals(['extra' => []], $record); } - private function mockGetResponseEvent(Request $request): GetResponseEvent + private function getRequestEvent(Request $request, int $requestType = HttpKernelInterface::MASTER_REQUEST): RequestEvent { - $event = $this->getMockBuilder(GetResponseEvent::class)->disableOriginalConstructor()->getMock(); - $event->method('getRequest')->willReturn($request); - - return $event; + return new RequestEvent($this->createMock(HttpKernelInterface::class), $request, $requestType); } - private function mockFinishRequestEvent(Request $request): FinishRequestEvent + private function getFinishRequestEvent(Request $request): FinishRequestEvent { - $event = $this->getMockBuilder(FinishRequestEvent::class)->disableOriginalConstructor()->getMock(); - $event->method('getRequest')->willReturn($request); - - return $event; + return new FinishRequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); } private function mockEmptyRequest(): Request @@ -154,7 +149,7 @@ private function mockFilledRequest(string $controller = self::TEST_CONTROLLER): private function mockRequest(array $attributes): Request { - $request = $this->getMockBuilder(Request::class)->disableOriginalConstructor()->getMock(); + $request = $this->createMock(Request::class); $request->attributes = new ParameterBag($attributes); return $request; diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php index ef3f6cc9f5c6a..dcaf0f647e301 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/TokenProcessorTest.php @@ -26,7 +26,7 @@ class TokenProcessorTest extends TestCase public function testProcessor() { $token = new UsernamePasswordToken('user', 'password', 'provider', ['ROLE_USER']); - $tokenStorage = $this->getMockBuilder(TokenStorageInterface::class)->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $tokenStorage->method('getToken')->willReturn($token); $processor = new TokenProcessor($tokenStorage); diff --git a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php index 54dbeee08f73b..73cb964d9ab6c 100644 --- a/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php +++ b/src/Symfony/Bridge/Monolog/Tests/Processor/WebProcessorTest.php @@ -16,12 +16,13 @@ use Symfony\Bridge\Monolog\Processor\WebProcessor; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\HttpKernelInterface; class WebProcessorTest extends TestCase { public function testUsesRequestServerData() { - list($event, $server) = $this->createRequestEvent(); + [$event, $server] = $this->createRequestEvent(); $processor = new WebProcessor(); $processor->onKernelRequest($event); @@ -38,7 +39,7 @@ public function testUsesRequestServerData() public function testUseRequestClientIp() { Request::setTrustedProxies(['192.168.0.1'], Request::HEADER_X_FORWARDED_ALL); - list($event, $server) = $this->createRequestEvent(['X_FORWARDED_FOR' => '192.168.0.2']); + [$event, $server] = $this->createRequestEvent(['X_FORWARDED_FOR' => '192.168.0.2']); $processor = new WebProcessor(); $processor->onKernelRequest($event); @@ -60,7 +61,7 @@ public function testCanBeConstructedWithExtraFields() $this->markTestSkipped('WebProcessor of the installed Monolog version does not support $extraFields parameter'); } - list($event, $server) = $this->createRequestEvent(); + [$event, $server] = $this->createRequestEvent(); $processor = new WebProcessor(['url', 'referrer']); $processor->onKernelRequest($event); @@ -71,7 +72,7 @@ public function testCanBeConstructedWithExtraFields() $this->assertEquals($server['HTTP_REFERER'], $record['extra']['referrer']); } - private function createRequestEvent($additionalServerParameters = []): array + private function createRequestEvent(array $additionalServerParameters = []): array { $server = array_merge( [ @@ -88,15 +89,7 @@ private function createRequestEvent($additionalServerParameters = []): array $request->server->replace($server); $request->headers->replace($server); - $event = $this->getMockBuilder(RequestEvent::class) - ->disableOriginalConstructor() - ->getMock(); - $event->expects($this->any()) - ->method('isMasterRequest') - ->willReturn(true); - $event->expects($this->any()) - ->method('getRequest') - ->willReturn($request); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); return [$event, $server]; } @@ -116,7 +109,7 @@ private function getRecord(int $level = Logger::WARNING, string $message = 'test private function isExtraFieldsSupported() { - $monologWebProcessorClass = new \ReflectionClass('Monolog\Processor\WebProcessor'); + $monologWebProcessorClass = new \ReflectionClass(\Monolog\Processor\WebProcessor::class); foreach ($monologWebProcessorClass->getConstructor()->getParameters() as $parameter) { if ('extraFields' === $parameter->getName()) { diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 846d427b756c1..613b619b450ed 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/monolog-bridge", "type": "symfony-bridge", - "description": "Symfony Monolog Bridge", + "description": "Provides integration for Monolog with various Symfony components", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,13 +16,15 @@ } ], "require": { - "php": "^7.1.3", - "monolog/monolog": "~1.19", - "symfony/service-contracts": "^1.1", - "symfony/http-kernel": "^4.3" + "php": ">=7.1.3", + "monolog/monolog": "^1.25.1", + "symfony/service-contracts": "^1.1|^2", + "symfony/http-kernel": "^4.3", + "symfony/polyfill-php80": "^1.16" }, "require-dev": { "symfony/console": "^3.4|^4.0|^5.0", + "symfony/http-client": "^4.4|^5.0", "symfony/security-core": "^3.4|^4.0|^5.0", "symfony/var-dumper": "^3.4|^4.0|^5.0" }, @@ -41,10 +43,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bridge/PhpUnit/.gitattributes b/src/Symfony/Bridge/PhpUnit/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md index 8cd3c372f2298..e9fc8f43c6e86 100644 --- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md +++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md @@ -1,11 +1,18 @@ CHANGELOG ========= +4.4.0 +----- + + * made the bridge act as a polyfill for newest PHPUnit features + * added `SetUpTearDownTrait` to allow working around the `void` return-type added by PHPUnit 8 + * added namespace aliases for PHPUnit < 6 + 4.3.0 ----- * added `ClassExistsMock` - * bumped PHP version from 5.3.3 to 5.5.9 + * bumped PHP version from 5.3.3 to 5.5.9 * split simple-phpunit bin into php file with code and a shell script 4.1.0 diff --git a/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php b/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php index e8ca4ac9402a8..d61d7887be891 100644 --- a/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClassExistsMock.php @@ -30,17 +30,23 @@ public static function withMockedClasses(array $classes) public static function class_exists($name, $autoload = true) { - return (bool) (self::$classes[ltrim($name, '\\')] ?? \class_exists($name, $autoload)); + $name = ltrim($name, '\\'); + + return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \class_exists($name, $autoload); } public static function interface_exists($name, $autoload = true) { - return (bool) (self::$classes[ltrim($name, '\\')] ?? \interface_exists($name, $autoload)); + $name = ltrim($name, '\\'); + + return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \interface_exists($name, $autoload); } public static function trait_exists($name, $autoload = true) { - return (bool) (self::$classes[ltrim($name, '\\')] ?? \trait_exists($name, $autoload)); + $name = ltrim($name, '\\'); + + return isset(self::$classes[$name]) ? (bool) self::$classes[$name] : \trait_exists($name, $autoload); } public static function register($class) diff --git a/src/Symfony/Bridge/PhpUnit/ClockMock.php b/src/Symfony/Bridge/PhpUnit/ClockMock.php index 534d906c1239f..2cc834cd4f679 100644 --- a/src/Symfony/Bridge/PhpUnit/ClockMock.php +++ b/src/Symfony/Bridge/PhpUnit/ClockMock.php @@ -26,6 +26,8 @@ public static function withClockMock($enable = null) } self::$now = is_numeric($enable) ? (float) $enable : ($enable ? microtime(true) : null); + + return null; } public static function time() @@ -51,10 +53,10 @@ public static function sleep($s) public static function usleep($us) { if (null === self::$now) { - return \usleep($us); + \usleep($us); + } else { + self::$now += $us / 1000000; } - - self::$now += $us / 1000000; } public static function microtime($asFloat = false) @@ -92,7 +94,7 @@ public static function register($class) { $self = \get_called_class(); - $mockedNs = array(substr($class, 0, strrpos($class, '\\'))); + $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (0 < strpos($class, '\\Tests\\')) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); @@ -123,7 +125,7 @@ function sleep(\$s) function usleep(\$us) { - return \\$self::usleep(\$us); + \\$self::usleep(\$us); } function date(\$format, \$timestamp = null) diff --git a/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php b/src/Symfony/Bridge/PhpUnit/ConstraintTrait.php new file mode 100644 index 0000000000000..cecac95ab56d4 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/ConstraintTrait.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\Bridge\PhpUnit; + +use PHPUnit\Framework\Constraint\Constraint; +use ReflectionClass; + +$r = new ReflectionClass(Constraint::class); +if (\PHP_VERSION_ID < 70000 || !$r->getMethod('matches')->hasReturnType()) { + trait ConstraintTrait + { + use Legacy\ConstraintTraitForV6; + } +} elseif ($r->getProperty('exporter')->isProtected()) { + trait ConstraintTrait + { + use Legacy\ConstraintTraitForV7; + } +} elseif (\PHP_VERSION_ID < 70100 || !$r->getMethod('evaluate')->hasReturnType()) { + trait ConstraintTrait + { + use Legacy\ConstraintTraitForV8; + } +} else { + trait ConstraintTrait + { + use Legacy\ConstraintTraitForV9; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/CoverageListener.php b/src/Symfony/Bridge/PhpUnit/CoverageListener.php index d41c26968fad3..805f9222a50d9 100644 --- a/src/Symfony/Bridge/PhpUnit/CoverageListener.php +++ b/src/Symfony/Bridge/PhpUnit/CoverageListener.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\PhpUnit; -if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { +if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListenerForV5', 'Symfony\Bridge\PhpUnit\CoverageListener'); } elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\CoverageListenerForV6', 'Symfony\Bridge\PhpUnit\CoverageListener'); diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index f9457c937d531..d81e36149273d 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -11,8 +11,12 @@ namespace Symfony\Bridge\PhpUnit; +use PHPUnit\Framework\TestResult; +use PHPUnit\Util\Error\Handler; +use PHPUnit\Util\ErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Component\ErrorHandler\DebugClassLoader; /** * Catch deprecation notices and print a summary report at the end of the test suite. @@ -48,7 +52,7 @@ class DeprecationErrorHandler ]; private static $isRegistered = false; - private static $utilPrefix; + private static $errorHandler; /** * Registers and configures the deprecation handler. @@ -72,15 +76,13 @@ public static function register($mode = 0) return; } - self::$utilPrefix = class_exists('PHPUnit_Util_ErrorHandler') ? 'PHPUnit_Util_' : 'PHPUnit\Util\\'; - $handler = new self(); $oldErrorHandler = set_error_handler([$handler, 'handleError']); if (null !== $oldErrorHandler) { restore_error_handler(); - if ([self::$utilPrefix.'ErrorHandler', 'handleError'] === $oldErrorHandler) { + if ($oldErrorHandler instanceof ErrorHandler || [ErrorHandler::class, 'handleError'] === $oldErrorHandler) { restore_error_handler(); self::register($mode); } @@ -95,20 +97,26 @@ public static function collectDeprecations($outputFile) { $deprecations = []; $previousErrorHandler = set_error_handler(function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$previousErrorHandler) { - if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) { if ($previousErrorHandler) { return $previousErrorHandler($type, $msg, $file, $line, $context); } - static $autoload = true; + return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context); + } - $ErrorHandler = class_exists('PHPUnit_Util_ErrorHandler', $autoload) ? 'PHPUnit_Util_ErrorHandler' : 'PHPUnit\Util\ErrorHandler'; - $autoload = false; + $filesStack = []; + foreach (debug_backtrace() as $frame) { + if (!isset($frame['file']) || \in_array($frame['function'], ['require', 'require_once', 'include', 'include_once'], true)) { + continue; + } - return $ErrorHandler::handleError($type, $msg, $file, $line, $context); + $filesStack[] = $frame['file']; } - $deprecations[] = [error_reporting(), $msg, $file]; + $deprecations[] = [error_reporting() & $type, $msg, $file, $filesStack]; + + return null; }); register_shutdown_function(function () use ($outputFile, &$deprecations) { @@ -121,50 +129,57 @@ public static function collectDeprecations($outputFile) */ public function handleError($type, $msg, $file, $line, $context = []) { - if ((E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) || !$this->getConfiguration()->isEnabled()) { - $ErrorHandler = self::$utilPrefix.'ErrorHandler'; + if ((\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type && (\E_WARNING !== $type || false === strpos($msg, '" targeting switch is equivalent to "break'))) || !$this->getConfiguration()->isEnabled()) { + return \call_user_func(self::getPhpUnitErrorHandler(), $type, $msg, $file, $line, $context); + } + + $trace = debug_backtrace(); - return $ErrorHandler::handleError($type, $msg, $file, $line, $context); + if (isset($trace[1]['function'], $trace[1]['args'][0]) && ('trigger_error' === $trace[1]['function'] || 'user_error' === $trace[1]['function'])) { + $msg = $trace[1]['args'][0]; } - $deprecation = new Deprecation($msg, debug_backtrace(), $file); - $group = 'other'; + $deprecation = new Deprecation($msg, $trace, $file, \E_DEPRECATED === $type); + if ($deprecation->isMuted()) { + return null; + } - if ($deprecation->originatesFromAnObject()) { - $class = $deprecation->originatingClass(); - $method = $deprecation->originatingMethod(); - $msg = $deprecation->getMessage(); + $msg = $deprecation->getMessage(); - if (0 !== error_reporting()) { - $group = 'unsilenced'; - } elseif ($deprecation->isLegacy(self::$utilPrefix)) { - $group = 'legacy'; - } else { - $group = [ - Deprecation::TYPE_SELF => 'remaining self', - Deprecation::TYPE_DIRECT => 'remaining direct', - Deprecation::TYPE_INDIRECT => 'remaining indirect', - Deprecation::TYPE_UNDETERMINED => 'other', - ][$deprecation->getType()]; - } + if (\E_DEPRECATED !== $type && (error_reporting() & $type)) { + $group = 'unsilenced'; + } elseif ($deprecation->isLegacy()) { + $group = 'legacy'; + } else { + $group = [ + Deprecation::TYPE_SELF => 'remaining self', + Deprecation::TYPE_DIRECT => 'remaining direct', + Deprecation::TYPE_INDIRECT => 'remaining indirect', + Deprecation::TYPE_UNDETERMINED => 'other', + ][$deprecation->getType()]; + } - if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) { - echo "\n".ucfirst($group).' '.$deprecation->toString(); + if ($this->getConfiguration()->shouldDisplayStackTrace($msg)) { + echo "\n".ucfirst($group).' '.$deprecation->toString(); - exit(1); - } - if ('legacy' !== $group) { - $ref = &$this->deprecations[$group][$msg]['count']; - ++$ref; + exit(1); + } + + if ('legacy' !== $group) { + $ref = &$this->deprecations[$group][$msg]['count']; + ++$ref; + + if ($deprecation->originatesFromAnObject()) { + $class = $deprecation->originatingClass(); + $method = $deprecation->originatingMethod(); $ref = &$this->deprecations[$group][$msg][$class.'::'.$method]; ++$ref; } - } else { - $ref = &$this->deprecations[$group][$msg]['count']; - ++$ref; } ++$this->deprecations[$group.'Count']; + + return null; } /** @@ -178,7 +193,10 @@ public function shutdown() return; } - $currErrorHandler = set_error_handler('var_dump'); + if (class_exists(DebugClassLoader::class, false)) { + DebugClassLoader::checkClasses(); + } + $currErrorHandler = set_error_handler('is_int'); restore_error_handler(); if ($currErrorHandler !== [$this, 'handleError']) { @@ -316,6 +334,36 @@ private function displayDeprecations($groups, $configuration) } } + private static function getPhpUnitErrorHandler() + { + if (!$eh = self::$errorHandler) { + if (class_exists(Handler::class)) { + $eh = self::$errorHandler = Handler::class; + } elseif (method_exists(ErrorHandler::class, '__invoke')) { + $eh = self::$errorHandler = ErrorHandler::class; + } else { + return self::$errorHandler = 'PHPUnit\Util\ErrorHandler::handleError'; + } + } + + if ('PHPUnit\Util\ErrorHandler::handleError' === $eh) { + return $eh; + } + + foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $frame) { + if (isset($frame['object']) && $frame['object'] instanceof TestResult) { + return new $eh( + $frame['object']->getConvertDeprecationsToExceptions(), + $frame['object']->getConvertErrorsToExceptions(), + $frame['object']->getConvertNoticesToExceptions(), + $frame['object']->getConvertWarningsToExceptions() + ); + } + } + + return function () { return false; }; + } + /** * Returns true if STDOUT is defined and supports colorization. * @@ -330,27 +378,32 @@ private static function hasColorSupport() return false; } + // Follow https://no-color.org/ + if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + return false; + } + if ('Hyper' === getenv('TERM_PROGRAM')) { return true; } if (\DIRECTORY_SEPARATOR === '\\') { return (\function_exists('sapi_windows_vt100_support') - && sapi_windows_vt100_support(STDOUT)) + && sapi_windows_vt100_support(\STDOUT)) || false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI') || 'xterm' === getenv('TERM'); } if (\function_exists('stream_isatty')) { - return stream_isatty(STDOUT); + return @stream_isatty(\STDOUT); } if (\function_exists('posix_isatty')) { - return posix_isatty(STDOUT); + return @posix_isatty(\STDOUT); } - $stat = fstat(STDOUT); + $stat = fstat(\STDOUT); // Check if formatted mode is S_IFCHR return $stat ? 0020000 === ($stat['mode'] & 0170000) : false; diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php index 6b42814bbc906..d26ffc45de692 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php @@ -48,10 +48,10 @@ private function __construct(array $thresholds = [], $regex = '', $verboseOutput foreach ($thresholds as $group => $threshold) { if (!\in_array($group, $groups, true)) { - throw new \InvalidArgumentException(sprintf('Unrecognized threshold "%s", expected one of "%s"', $group, implode('", "', $groups))); + throw new \InvalidArgumentException(sprintf('Unrecognized threshold "%s", expected one of "%s".', $group, implode('", "', $groups))); } if (!is_numeric($threshold)) { - throw new \InvalidArgumentException(sprintf('Threshold for group "%s" has invalid value "%s"', $group, $threshold)); + throw new \InvalidArgumentException(sprintf('Threshold for group "%s" has invalid value "%s".', $group, $threshold)); } $this->thresholds[$group] = (int) $threshold; } @@ -146,7 +146,7 @@ public static function fromUrlEncodedString($serializedConfiguration) parse_str($serializedConfiguration, $normalizedConfiguration); foreach (array_keys($normalizedConfiguration) as $key) { if (!\in_array($key, ['max', 'disabled', 'verbose'], true)) { - throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s"', $key)); + throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s".', $key)); } } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php index ea84256663528..9f4f593962235 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Deprecation.php @@ -11,7 +11,15 @@ namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; +use PHPUnit\Framework\TestCase; +use PHPUnit\Framework\TestSuite; +use PHPUnit\Metadata\Api\Groups; +use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerFor; +use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; +use Symfony\Component\ErrorHandler\DebugClassLoader; + +class_exists(Groups::class); /** * @internal @@ -27,74 +35,101 @@ class Deprecation const TYPE_INDIRECT = 'type_indirect'; const TYPE_UNDETERMINED = 'type_undetermined'; - /** - * @var array - */ - private $trace; - - /** - * @var string - */ + private $trace = []; private $message; - - /** - * @var ?string - */ + private $languageDeprecation; private $originClass; - - /** - * @var ?string - */ private $originMethod; + private $triggeringFile; - /** - * @var string one of the PATH_TYPE_* constants - */ - private $triggeringFilePathType; - - /** @var string[] absolute paths to vendor directories */ + /** @var string[] Absolute paths to vendor directories */ private static $vendors; /** - * @var string[] absolute paths to source or tests of the project. This - * excludes cache directories, because it is based on - * autoloading rules and cache systems typically do not use - * those. + * @var string[] Absolute paths to source or tests of the project, cache + * directories excluded because it is based on autoloading + * rules and cache systems typically do not use those */ - private static $internalPaths; + private static $internalPaths = []; + + private $originalFilesStack; /** * @param string $message * @param string $file + * @param bool $languageDeprecation */ - public function __construct($message, array $trace, $file) + public function __construct($message, array $trace, $file, $languageDeprecation = false) { $this->trace = $trace; $this->message = $message; + $this->languageDeprecation = $languageDeprecation; + $i = \count($trace); while (1 < $i && $this->lineShouldBeSkipped($trace[--$i])) { // No-op } + $line = $trace[$i]; - $this->triggeringFilePathType = $this->getPathType($file); - if (isset($line['object']) || isset($line['class'])) { - if (isset($line['class']) && 0 === strpos($line['class'], SymfonyTestsListenerFor::class)) { - $parsedMsg = unserialize($this->message); - $this->message = $parsedMsg['deprecation']; - $this->originClass = $parsedMsg['class']; - $this->originMethod = $parsedMsg['method']; - // If the deprecation has been triggered via - // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() - // then we need to use the serialized information to determine - // if the error has been triggered from vendor code. - if (isset($parsedMsg['triggering_file'])) { - $this->triggeringFilePathType = $this->getPathType($parsedMsg['triggering_file']); + $this->triggeringFile = $file; + + for ($j = 1; $j < $i; ++$j) { + if (!isset($trace[$j]['function'], $trace[1 + $j]['class'], $trace[1 + $j]['args'][0])) { + continue; + } + + if ('trigger_error' === $trace[$j]['function'] && !isset($trace[$j]['class'])) { + if (\in_array($trace[1 + $j]['class'], [DebugClassLoader::class, LegacyDebugClassLoader::class], true)) { + $class = $trace[1 + $j]['args'][0]; + $this->triggeringFile = isset($trace[1 + $j]['args'][1]) ? realpath($trace[1 + $j]['args'][1]) : (new \ReflectionClass($class))->getFileName(); + $this->getOriginalFilesStack(); + array_splice($this->originalFilesStack, 0, $j, [$this->triggeringFile]); + + if (preg_match('/(?|"([^"]++)" that is deprecated|should implement method "(?:static )?([^:]++))/', $message, $m) || preg_match('/^(?:The|Method) "([^":]++)/', $message, $m)) { + $this->triggeringFile = (new \ReflectionClass($m[1]))->getFileName(); + array_unshift($this->originalFilesStack, $this->triggeringFile); + } } - return; + break; } + } + + if (!isset($line['object']) && !isset($line['class'])) { + return; + } + + if (!isset($line['class'], $trace[$i - 2]['function']) || 0 !== strpos($line['class'], SymfonyTestsListenerFor::class)) { $this->originClass = isset($line['object']) ? \get_class($line['object']) : $line['class']; $this->originMethod = $line['function']; + + return; + } + + $test = isset($line['args'][0]) ? $line['args'][0] : null; + + if (($test instanceof TestCase || $test instanceof TestSuite) && ('trigger_error' !== $trace[$i - 2]['function'] || isset($trace[$i - 2]['class']))) { + $this->originClass = \get_class($test); + $this->originMethod = $test->getName(); + + return; + } + + set_error_handler(function () {}); + $parsedMsg = unserialize($this->message); + restore_error_handler(); + $this->message = $parsedMsg['deprecation']; + $this->originClass = $parsedMsg['class']; + $this->originMethod = $parsedMsg['method']; + if (isset($parsedMsg['files_stack'])) { + $this->originalFilesStack = $parsedMsg['files_stack']; + } + // If the deprecation has been triggered via + // \Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait::endTest() + // then we need to use the serialized information to determine + // if the error has been triggered from vendor code. + if (isset($parsedMsg['triggering_file'])) { + $this->triggeringFile = $parsedMsg['triggering_file']; } } @@ -125,10 +160,12 @@ public function originatesFromAnObject() public function originatingClass() { if (null === $this->originClass) { - throw new \LogicException('Check with originatesFromAnObject() before calling this method'); + throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); } - return $this->originClass; + $class = $this->originClass; + + return false !== strpos($class, "@anonymous\0") ? (get_parent_class($class) ?: key(class_implements($class)) ?: 'class').'@anonymous' : $class; } /** @@ -137,7 +174,7 @@ public function originatingClass() public function originatingMethod() { if (null === $this->originMethod) { - throw new \LogicException('Check with originatesFromAnObject() before calling this method'); + throw new \LogicException('Check with originatesFromAnObject() before calling this method.'); } return $this->originMethod; @@ -152,21 +189,37 @@ public function getMessage() } /** - * @param string $utilPrefix - * * @return bool */ - public function isLegacy($utilPrefix) + public function isLegacy() { - $test = $utilPrefix.'Test'; - $class = $this->originatingClass(); + if (!$this->originClass || (new \ReflectionClass($this->originClass))->isInternal()) { + return false; + } + $method = $this->originatingMethod(); + $groups = class_exists(Groups::class, false) ? [new Groups(), 'groups'] : [Test::class, 'getGroups']; return 0 === strpos($method, 'testLegacy') || 0 === strpos($method, 'provideLegacy') || 0 === strpos($method, 'getLegacy') - || strpos($class, '\Legacy') - || \in_array('legacy', $test::getGroups($class, $method), true); + || strpos($this->originClass, '\Legacy') + || \in_array('legacy', $groups($this->originClass, $method), true); + } + + /** + * @return bool + */ + public function isMuted() + { + if ('Function ReflectionType::__toString() is deprecated' !== $this->message) { + return false; + } + if (isset($this->trace[1]['class'])) { + return 0 === strpos($this->trace[1]['class'], 'PHPUnit\\'); + } + + return false !== strpos($this->triggeringFile, \DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'phpunit'.\DIRECTORY_SEPARATOR); } /** @@ -177,28 +230,27 @@ public function isLegacy($utilPrefix) */ public function getType() { - if (self::PATH_TYPE_SELF === $this->triggeringFilePathType) { + $pathType = $this->getPathType($this->triggeringFile); + if ($this->languageDeprecation && self::PATH_TYPE_VENDOR === $pathType) { + // the triggering file must be used for language deprecations + return self::TYPE_INDIRECT; + } + if (self::PATH_TYPE_SELF === $pathType) { return self::TYPE_SELF; } - if (self::PATH_TYPE_UNDETERMINED === $this->triggeringFilePathType) { + if (self::PATH_TYPE_UNDETERMINED === $pathType) { return self::TYPE_UNDETERMINED; } $erroringFile = $erroringPackage = null; - foreach ($this->trace as $line) { - if (\in_array($line['function'], ['require', 'require_once', 'include', 'include_once'], true)) { - continue; - } - if (!isset($line['file'])) { - continue; - } - $file = $line['file']; + + foreach ($this->getOriginalFilesStack() as $file) { if ('-' === $file || 'Standard input code' === $file || !realpath($file)) { continue; } - if (self::PATH_TYPE_SELF === $this->getPathType($file)) { + if (self::PATH_TYPE_SELF === $pathType = $this->getPathType($file)) { return self::TYPE_DIRECT; } - if (self::PATH_TYPE_UNDETERMINED === $this->getPathType($file)) { + if (self::PATH_TYPE_UNDETERMINED === $pathType) { return self::TYPE_UNDETERMINED; } if (null !== $erroringFile && null !== $erroringPackage) { @@ -215,6 +267,22 @@ public function getType() return self::TYPE_DIRECT; } + private function getOriginalFilesStack() + { + if (null === $this->originalFilesStack) { + $this->originalFilesStack = []; + foreach ($this->trace as $frame) { + if (!isset($frame['file'], $frame['function']) || (!isset($frame['class']) && \in_array($frame['function'], ['require', 'require_once', 'include', 'include_once'], true))) { + continue; + } + + $this->originalFilesStack[] = $frame['file']; + } + } + + return $this->originalFilesStack; + } + /** * getPathType() should always be called prior to calling this method. * @@ -230,17 +298,14 @@ private function getPackage($path) $relativePath = substr($path, \strlen($vendorRoot) + 1); $vendor = strstr($relativePath, \DIRECTORY_SEPARATOR, true); if (false === $vendor) { - throw new \RuntimeException(sprintf('Could not find directory separator "%s" in path "%s"', \DIRECTORY_SEPARATOR, $relativePath)); + return 'symfony'; } - return rtrim($vendor.'/'.strstr(substr( - $relativePath, - \strlen($vendor) + 1 - ), \DIRECTORY_SEPARATOR, true), '/'); + return rtrim($vendor.'/'.strstr(substr($relativePath, \strlen($vendor) + 1), \DIRECTORY_SEPARATOR, true), '/'); } } - throw new \RuntimeException(sprintf('No vendors found for path "%s"', $path)); + throw new \RuntimeException(sprintf('No vendors found for path "%s".', $path)); } /** @@ -249,7 +314,14 @@ private function getPackage($path) private static function getVendors() { if (null === self::$vendors) { - self::$vendors = []; + self::$vendors = $paths = []; + self::$vendors[] = \dirname(__DIR__).\DIRECTORY_SEPARATOR.'Legacy'; + if (class_exists(DebugClassLoader::class, false)) { + self::$vendors[] = \dirname((new \ReflectionClass(DebugClassLoader::class))->getFileName()); + } + if (class_exists(LegacyDebugClassLoader::class, false)) { + self::$vendors[] = \dirname((new \ReflectionClass(LegacyDebugClassLoader::class))->getFileName()); + } foreach (get_declared_classes() as $class) { if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); @@ -257,7 +329,10 @@ private static function getVendors() if (file_exists($v.'/composer/installed.json')) { self::$vendors[] = $v; $loader = require $v.'/autoload.php'; - $paths = self::getSourcePathsFromPrefixes(array_merge($loader->getPrefixes(), $loader->getPrefixesPsr4())); + $paths = self::addSourcePathsFromPrefixes( + array_merge($loader->getPrefixes(), $loader->getPrefixesPsr4()), + $paths + ); } } } @@ -273,15 +348,17 @@ private static function getVendors() return self::$vendors; } - private static function getSourcePathsFromPrefixes(array $prefixesByNamespace) + private static function addSourcePathsFromPrefixes(array $prefixesByNamespace, array $paths) { foreach ($prefixesByNamespace as $prefixes) { foreach ($prefixes as $prefix) { if (false !== realpath($prefix)) { - yield realpath($prefix); + $paths[] = realpath($prefix); } } } + + return $paths; } /** @@ -320,10 +397,9 @@ public function toString() $reflection->setAccessible(true); $reflection->setValue($exception, $this->trace); - return 'deprecation triggered by '.$this->originatingClass().'::'.$this->originatingMethod().':'. - "\n".$this->message. - "\nStack trace:". - "\n".str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $exception->getTraceAsString()). - "\n"; + return ($this->originatesFromAnObject() ? 'deprecation triggered by '.$this->originatingClass().'::'.$this->originatingMethod().":\n" : '') + .$this->message."\n" + ."Stack trace:\n" + .str_replace(' '.getcwd().\DIRECTORY_SEPARATOR, ' ', $exception->getTraceAsString())."\n"; } } diff --git a/src/Symfony/Bridge/PhpUnit/DnsMock.php b/src/Symfony/Bridge/PhpUnit/DnsMock.php index 790cfa91af5c2..1e2f55b371be3 100644 --- a/src/Symfony/Bridge/PhpUnit/DnsMock.php +++ b/src/Symfony/Bridge/PhpUnit/DnsMock.php @@ -16,21 +16,21 @@ */ class DnsMock { - private static $hosts = array(); - private static $dnsTypes = array( - 'A' => DNS_A, - 'MX' => DNS_MX, - 'NS' => DNS_NS, - 'SOA' => DNS_SOA, - 'PTR' => DNS_PTR, - 'CNAME' => DNS_CNAME, - 'AAAA' => DNS_AAAA, - 'A6' => DNS_A6, - 'SRV' => DNS_SRV, - 'NAPTR' => DNS_NAPTR, - 'TXT' => DNS_TXT, - 'HINFO' => DNS_HINFO, - ); + private static $hosts = []; + private static $dnsTypes = [ + 'A' => \DNS_A, + 'MX' => \DNS_MX, + 'NS' => \DNS_NS, + 'SOA' => \DNS_SOA, + 'PTR' => \DNS_PTR, + 'CNAME' => \DNS_CNAME, + 'AAAA' => \DNS_AAAA, + 'A6' => \DNS_A6, + 'SRV' => \DNS_SRV, + 'NAPTR' => \DNS_NAPTR, + 'TXT' => \DNS_TXT, + 'HINFO' => \DNS_HINFO, + ]; /** * Configures the mock values for DNS queries. @@ -68,7 +68,7 @@ public static function getmxrr($hostname, &$mxhosts, &$weight = null) if (!self::$hosts) { return \getmxrr($hostname, $mxhosts, $weight); } - $mxhosts = $weight = array(); + $mxhosts = $weight = []; if (isset(self::$hosts[$hostname])) { foreach (self::$hosts[$hostname] as $record) { @@ -125,7 +125,7 @@ public static function gethostbynamel($hostname) $ips = false; if (isset(self::$hosts[$hostname])) { - $ips = array(); + $ips = []; foreach (self::$hosts[$hostname] as $record) { if ('A' === $record['type']) { @@ -137,7 +137,7 @@ public static function gethostbynamel($hostname) return $ips; } - public static function dns_get_record($hostname, $type = DNS_ANY, &$authns = null, &$addtl = null, $raw = false) + public static function dns_get_record($hostname, $type = \DNS_ANY, &$authns = null, &$addtl = null, $raw = false) { if (!self::$hosts) { return \dns_get_record($hostname, $type, $authns, $addtl, $raw); @@ -146,14 +146,14 @@ public static function dns_get_record($hostname, $type = DNS_ANY, &$authns = nul $records = false; if (isset(self::$hosts[$hostname])) { - if (DNS_ANY === $type) { - $type = DNS_ALL; + if (\DNS_ANY === $type) { + $type = \DNS_ALL; } - $records = array(); + $records = []; foreach (self::$hosts[$hostname] as $record) { if (isset(self::$dnsTypes[$record['type']]) && (self::$dnsTypes[$record['type']] & $type)) { - $records[] = array_merge(array('host' => $hostname, 'class' => 'IN', 'ttl' => 1, 'type' => $record['type']), $record); + $records[] = array_merge(['host' => $hostname, 'class' => 'IN', 'ttl' => 1, 'type' => $record['type']], $record); } } } @@ -165,7 +165,7 @@ public static function register($class) { $self = \get_called_class(); - $mockedNs = array(substr($class, 0, strrpos($class, '\\'))); + $mockedNs = [substr($class, 0, strrpos($class, '\\'))]; if (0 < strpos($class, '\\Tests\\')) { $ns = str_replace('\\Tests\\', '\\', $class); $mockedNs[] = substr($ns, 0, strrpos($ns, '\\')); diff --git a/src/Symfony/Bridge/PhpUnit/LICENSE b/src/Symfony/Bridge/PhpUnit/LICENSE index cf8b3ebe87145..a843ec124ea70 100644 --- a/src/Symfony/Bridge/PhpUnit/LICENSE +++ b/src/Symfony/Bridge/PhpUnit/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php index 95dcb1e5541fc..2ce390df38609 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV5.php @@ -23,9 +23,7 @@ class CommandForV5 extends \PHPUnit_TextUI_Command */ protected function createRunner() { - $listener = new SymfonyTestsListenerForV5(); - - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : array(); + $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; $registeredLocally = false; @@ -37,8 +35,21 @@ protected function createRunner() } } + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + if (!$configuration instanceof \PHPUnit_Util_Configuration) { + $configuration = \PHPUnit_Util_Configuration::getInstance($this->arguments['configuration']); + } + foreach ($configuration->getListenerConfiguration() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { + $registeredLocally = true; + break; + } + } + } + if (!$registeredLocally) { - $this->arguments['listeners'][] = $listener; + $this->arguments['listeners'][] = new SymfonyTestsListenerForV5(); } return parent::createRunner(); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php index f8f75bb09a93b..93e1ad975b7e4 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV6.php @@ -13,6 +13,7 @@ use PHPUnit\TextUI\Command as BaseCommand; use PHPUnit\TextUI\TestRunner as BaseRunner; +use PHPUnit\Util\Configuration; use Symfony\Bridge\PhpUnit\SymfonyTestsListener; /** @@ -27,8 +28,6 @@ class CommandForV6 extends BaseCommand */ protected function createRunner(): BaseRunner { - $listener = new SymfonyTestsListener(); - $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; $registeredLocally = false; @@ -41,8 +40,21 @@ protected function createRunner(): BaseRunner } } + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + if (!$configuration instanceof Configuration) { + $configuration = Configuration::getInstance($this->arguments['configuration']); + } + foreach ($configuration->getListenerConfiguration() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener['class'], '\\')) { + $registeredLocally = true; + break; + } + } + } + if (!$registeredLocally) { - $this->arguments['listeners'][] = $listener; + $this->arguments['listeners'][] = new SymfonyTestsListener(); } return parent::createRunner(); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.php new file mode 100644 index 0000000000000..2511380257fd8 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CommandForV9.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\Bridge\PhpUnit\Legacy; + +use PHPUnit\TextUI\Command as BaseCommand; +use PHPUnit\TextUI\Configuration\Configuration as LegacyConfiguration; +use PHPUnit\TextUI\Configuration\Registry; +use PHPUnit\TextUI\TestRunner as BaseRunner; +use PHPUnit\TextUI\XmlConfiguration\Configuration; +use PHPUnit\TextUI\XmlConfiguration\Loader; +use Symfony\Bridge\PhpUnit\SymfonyTestsListener; + +/** + * {@inheritdoc} + * + * @internal + */ +class CommandForV9 extends BaseCommand +{ + /** + * {@inheritdoc} + */ + protected function createRunner(): BaseRunner + { + $this->arguments['listeners'] = isset($this->arguments['listeners']) ? $this->arguments['listeners'] : []; + + $registeredLocally = false; + + foreach ($this->arguments['listeners'] as $registeredListener) { + if ($registeredListener instanceof SymfonyTestsListener) { + $registeredListener->globalListenerDisabled(); + $registeredLocally = true; + break; + } + } + + if (isset($this->arguments['configuration'])) { + $configuration = $this->arguments['configuration']; + + if (!class_exists(Configuration::class) && !$configuration instanceof LegacyConfiguration) { + $configuration = Registry::getInstance()->get($this->arguments['configuration']); + } elseif (class_exists(Configuration::class) && !$configuration instanceof Configuration) { + $configuration = (new Loader())->load($this->arguments['configuration']); + } + + foreach ($configuration->listeners() as $registeredListener) { + if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener->className(), '\\')) { + $registeredLocally = true; + break; + } + } + } + + if (!$registeredLocally) { + $this->arguments['listeners'][] = new SymfonyTestsListener(); + } + + return parent::createRunner(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintLogicTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintLogicTrait.php new file mode 100644 index 0000000000000..e124358c4f724 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintLogicTrait.php @@ -0,0 +1,62 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +/** + * @internal + */ +trait ConstraintLogicTrait +{ + private function doEvaluate($other, $description, $returnResult) + { + $success = false; + + if ($this->matches($other)) { + $success = true; + } + + if ($returnResult) { + return $success; + } + + if (!$success) { + $this->fail($other, $description); + } + + return null; + } + + private function doAdditionalFailureDescription($other): string + { + return ''; + } + + private function doCount(): int + { + return 1; + } + + private function doFailureDescription($other): string + { + return $this->exporter()->export($other).' '.$this->toString(); + } + + private function doMatches($other): bool + { + return false; + } + + private function doToString(): string + { + return ''; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.php new file mode 100644 index 0000000000000..53819e4b3c4d7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV6.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\Bridge\PhpUnit\Legacy; + +use SebastianBergmann\Exporter\Exporter; + +/** + * @internal + */ +trait ConstraintTraitForV6 +{ + /** + * @return bool|null + */ + public function evaluate($other, $description = '', $returnResult = false) + { + return $this->doEvaluate($other, $description, $returnResult); + } + + /** + * @return int + */ + public function count() + { + return $this->doCount(); + } + + /** + * @return string + */ + public function toString() + { + return $this->doToString(); + } + + /** + * @param mixed $other + * + * @return string + */ + protected function additionalFailureDescription($other) + { + return $this->doAdditionalFailureDescription($other); + } + + /** + * @return Exporter + */ + protected function exporter() + { + if (null === $this->exporter) { + $this->exporter = new Exporter(); + } + + return $this->exporter; + } + + /** + * @param mixed $other + * + * @return string + */ + protected function failureDescription($other) + { + return $this->doFailureDescription($other); + } + + /** + * @param mixed $other + * + * @return bool + */ + protected function matches($other) + { + return $this->doMatches($other); + } + + private function doAdditionalFailureDescription($other) + { + return ''; + } + + private function doCount() + { + return 1; + } + + private function doEvaluate($other, $description, $returnResult) + { + $success = false; + + if ($this->matches($other)) { + $success = true; + } + + if ($returnResult) { + return $success; + } + + if (!$success) { + $this->fail($other, $description); + } + + return null; + } + + private function doFailureDescription($other) + { + return $this->exporter()->export($other).' '.$this->toString(); + } + + private function doMatches($other) + { + return false; + } + + private function doToString() + { + return ''; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV7.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV7.php new file mode 100644 index 0000000000000..b132f473c547e --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV7.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\Bridge\PhpUnit\Legacy; + +use SebastianBergmann\Exporter\Exporter; + +/** + * @internal + */ +trait ConstraintTraitForV7 +{ + use ConstraintLogicTrait; + + /** + * @return bool|null + */ + public function evaluate($other, $description = '', $returnResult = false) + { + return $this->doEvaluate($other, $description, $returnResult); + } + + public function count(): int + { + return $this->doCount(); + } + + public function toString(): string + { + return $this->doToString(); + } + + protected function additionalFailureDescription($other): string + { + return $this->doAdditionalFailureDescription($other); + } + + protected function exporter(): Exporter + { + if (null === $this->exporter) { + $this->exporter = new Exporter(); + } + + return $this->exporter; + } + + protected function failureDescription($other): string + { + return $this->doFailureDescription($other); + } + + protected function matches($other): bool + { + return $this->doMatches($other); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV8.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV8.php new file mode 100644 index 0000000000000..d31cc1215877b --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV8.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\Bridge\PhpUnit\Legacy; + +/** + * @internal + */ +trait ConstraintTraitForV8 +{ + use ConstraintLogicTrait; + + /** + * @return bool|null + */ + public function evaluate($other, $description = '', $returnResult = false) + { + return $this->doEvaluate($other, $description, $returnResult); + } + + public function count(): int + { + return $this->doCount(); + } + + public function toString(): string + { + return $this->doToString(); + } + + protected function additionalFailureDescription($other): string + { + return $this->doAdditionalFailureDescription($other); + } + + protected function failureDescription($other): string + { + return $this->doFailureDescription($other); + } + + protected function matches($other): bool + { + return $this->doMatches($other); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV9.php b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV9.php new file mode 100644 index 0000000000000..66da873e4243e --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/ConstraintTraitForV9.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\Bridge\PhpUnit\Legacy; + +/** + * @internal + */ +trait ConstraintTraitForV9 +{ + use ConstraintLogicTrait; + + public function evaluate($other, string $description = '', bool $returnResult = false): ?bool + { + return $this->doEvaluate($other, $description, $returnResult); + } + + public function count(): int + { + return $this->doCount(); + } + + public function toString(): string + { + return $this->doToString(); + } + + protected function additionalFailureDescription($other): string + { + return $this->doAdditionalFailureDescription($other); + } + + protected function failureDescription($other): string + { + return $this->doFailureDescription($other); + } + + protected function matches($other): bool + { + return $this->doMatches($other); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php index 0917ea4710c21..1b3ceec161f8a 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerForV6.php @@ -11,8 +11,9 @@ namespace Symfony\Bridge\PhpUnit\Legacy; -use PHPUnit\Framework\BaseTestListener; use PHPUnit\Framework\Test; +use PHPUnit\Framework\TestListener; +use PHPUnit\Framework\TestListenerDefaultImplementation; /** * CoverageListener adds `@covers ` on each test when possible to @@ -22,8 +23,10 @@ * * @internal */ -class CoverageListenerForV6 extends BaseTestListener +class CoverageListenerForV6 implements TestListener { + use TestListenerDefaultImplementation; + private $trait; public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFound = false) diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php index d68aa2888218b..4ca396ece164b 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php @@ -13,6 +13,8 @@ use PHPUnit\Framework\TestCase; use PHPUnit\Framework\Warning; +use PHPUnit\Util\Annotation\Registry; +use PHPUnit\Util\Test; /** * PHP 5.3 compatible trait-like shared implementation. @@ -31,7 +33,7 @@ public function __construct(callable $sutFqcnResolver = null, $warningOnSutNotFo { $this->sutFqcnResolver = $sutFqcnResolver; $this->warningOnSutNotFound = $warningOnSutNotFound; - $this->warnings = array(); + $this->warnings = []; } public function startTest($test) @@ -40,9 +42,9 @@ public function startTest($test) return; } - $annotations = $test->getAnnotations(); + $annotations = Test::parseTestMethodAnnotations(\get_class($test) 10000 , $test->getName(false)); - $ignoredAnnotations = array('covers', 'coversDefaultClass', 'coversNothing'); + $ignoredAnnotations = ['covers', 'coversDefaultClass', 'coversNothing']; foreach ($ignoredAnnotations as $annotation) { if (isset($annotations['class'][$annotation]) || isset($annotations['method'][$annotation])) { @@ -65,21 +67,56 @@ public function startTest($test) return; } - $testClass = \PHPUnit\Util\Test::class; - if (!class_exists($testClass, false)) { - $testClass = \PHPUnit_Util_Test::class; + $covers = $sutFqcn; + if (!\is_array($sutFqcn)) { + $covers = [$sutFqcn]; + while ($parent = get_parent_class($sutFqcn)) { + $covers[] = $parent; + $sutFqcn = $parent; + } + } + + if (class_exists(Registry::class)) { + $this->addCoversForDocBlockInsideRegistry($test, $covers); + + return; } - $r = new \ReflectionProperty($testClass, 'annotationCache'); + $this->addCoversForClassToAnnotationCache($test, $covers); + } + + private function addCoversForClassToAnnotationCache($test, $covers) + { + $r = new \ReflectionProperty(Test::class, 'annotationCache'); $r->setAccessible(true); $cache = $r->getValue(); - $cache = array_replace_recursive($cache, array( - \get_class($test) => array( - 'covers' => array($sutFqcn), - ), - )); - $r->setValue($testClass, $cache); + $cache = array_replace_recursive($cache, [ + \get_class($test) => [ + 'covers' => $covers, + ], + ]); + + $r->setValue(Test::class, $cache); + } + + private function addCoversForDocBlockInsideRegistry($test, $covers) + { + $docBlock = Registry::getInstance()->forClassName(\get_class($test)); + + $symbolAnnotations = new \ReflectionProperty($docBlock, 'symbolAnnotations'); + $symbolAnnotations->setAccessible(true); + + // Exclude internal classes; PHPUnit 9.1+ is picky about tests covering, say, a \RuntimeException + $covers = array_filter($covers, function ($class) { + $reflector = new \ReflectionClass($class); + + return $reflector->isUserDefined(); + }); + + $symbolAnnotations->setValue($docBlock, array_replace($docBlock->symbolAnnotations(), [ + 'covers' => $covers, + ])); } private function findSutFqcn($test) @@ -95,11 +132,7 @@ private function findSutFqcn($test) $sutFqcn = str_replace('\\Tests\\', '\\', $class); $sutFqcn = preg_replace('{Test$}', '', $sutFqcn); - if (!class_exists($sutFqcn)) { - return; - } - - return $sutFqcn; + return class_exists($sutFqcn) ? $sutFqcn : null; } public function __sleep() diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php new file mode 100644 index 0000000000000..5a66282d855ca --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillAssertTrait.php @@ -0,0 +1,558 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Legacy; + +use PHPUnit\Framework\Constraint\IsEqual; +use PHPUnit\Framework\Constraint\LogicalNot; +use PHPUnit\Framework\Constraint\StringContains; +use PHPUnit\Framework\Constraint\TraversableContains; + +/** + * This trait is @internal. + */ +trait PolyfillAssertTrait +{ + /** + * @param float $delta + * @param string $message + * + * @return void + */ + public static function assertEqualsWithDelta($expected, $actual, $delta, $message = '') + { + $constraint = new IsEqual($expected, $delta); + static::assertThat($actual, $constraint, $message); + } + + /** + * @param iterable $haystack + * @param string $message + * + * @return void + */ + public static function assertContainsEquals($needle, $haystack, $message = '') + { + $constraint = new TraversableContains($needle, false, false); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param iterable $haystack + * @param string $message + * + * @return void + */ + public static function assertNotContainsEquals($needle, $haystack, $message = '') + { + $constraint = new LogicalNot(new TraversableContains($needle, false, false)); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsArray($actual, $message = '') + { + static::assertInternalType('array', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsBool($actual, $message = '') + { + static::assertInternalType('bool', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsFloat($actual, $message = '') + { + static::assertInternalType('float', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsInt($actual, $message = '') + { + static::assertInternalType('int', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsNumeric($actual, $message = '') + { + static::assertInternalType('numeric', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsObject($actual, $message = '') + { + static::assertInternalType('object', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsResource($actual, $message = '') + { + static::assertInternalType('resource', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsString($actual, $message = '') + { + static::assertInternalType('string', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsScalar($actual, $message = '') + { + static::assertInternalType('scalar', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsCallable($actual, $message = '') + { + static::assertInternalType('callable', $actual, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertIsIterable($actual, $message = '') + { + static::assertInternalType('iterable', $actual, $message); + } + + /** + * @param string $needle + * @param string $haystack + * @param string $message + * + * @return void + */ + public static function assertStringContainsString($needle, $haystack, $message = '') + { + $constraint = new StringContains($needle, false); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param string $needle + * @param string $haystack + * @param string $message + * + * @return void + */ + public static function assertStringContainsStringIgnoringCase($needle, $haystack, $message = '') + { + $constraint = new StringContains($needle, true); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param string $needle + * @param string $haystack + * @param string $message + * + * @return void + */ + public static function assertStringNotContainsString($needle, $haystack, $message = '') + { + $constraint = new LogicalNot(new StringContains($needle, false)); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param string $needle + * @param string $haystack + * @param string $message + * + * @return void + */ + public static function assertStringNotContainsStringIgnoringCase($needle, $haystack, $message = '') + { + $constraint = new LogicalNot(new StringContains($needle, true)); + static::assertThat($haystack, $constraint, $message); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertFinite($actual, $message = '') + { + static::assertInternalType('float', $actual, $message); + static::assertTrue(is_finite($actual), $message ?: "Failed asserting that $actual is finite."); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertInfinite($actual, $message = '') + { + static::assertInternalType('float', $actual, $message); + static::assertTrue(is_infinite($actual), $message ?: "Failed asserting that $actual is infinite."); + } + + /** + * @param string $message + * + * @return void + */ + public static function assertNan($actual, $message = '') + { + static::assertInternalType('float', $actual, $message); + static::assertTrue(is_nan($actual), $message ?: "Failed asserting that $actual is nan."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertIsReadable($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertTrue(is_readable($filename), $message ?: "Failed asserting that $filename is readable."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertNotIsReadable($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertFalse(is_readable($filename), $message ?: "Failed asserting that $filename is not readable."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertIsNotReadable($filename, $message = '') + { + static::assertNotIsReadable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertIsWritable($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertTrue(is_writable($filename), $message ?: "Failed asserting that $filename is writable."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertNotIsWritable($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertFalse(is_writable($filename), $message ?: "Failed asserting that $filename is not writable."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertIsNotWritable($filename, $message = '') + { + static::assertNotIsWritable($filename, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryExists($directory, $message = '') + { + static::assertInternalType('string', $directory, $message); + static::assertTrue(is_dir($directory), $message ?: "Failed asserting that $directory exists."); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryNotExists($directory, $message = '') + { + static::assertInternalType('string', $directory, $message); + static::assertFalse(is_dir($directory), $message ?: "Failed asserting that $directory does not exist."); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryDoesNotExist($directory, $message = '') + { + static::assertDirectoryNotExists($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryIsReadable($directory, $message = '') + { + static::assertDirectoryExists($directory, $message); + static::assertIsReadable($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryNotIsReadable($directory, $message = '') + { + static::assertDirectoryExists($directory, $message); + static::assertNotIsReadable($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryIsNotReadable($directory, $message = '') + { + static::assertDirectoryNotIsReadable($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryIsWritable($directory, $message = '') + { + static::assertDirectoryExists($directory, $message); + static::assertIsWritable($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryNotIsWritable($directory, $message = '') + { + static::assertDirectoryExists($directory, $message); + static::assertNotIsWritable($directory, $message); + } + + /** + * @param string $directory + * @param string $message + * + * @return void + */ + public static function assertDirectoryIsNotWritable($directory, $message = '') + { + static::assertDirectoryNotIsWritable($directory, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileExists($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertTrue(file_exists($filename), $message ?: "Failed asserting that $filename exists."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileNotExists($filename, $message = '') + { + static::assertInternalType('string', $filename, $message); + static::assertFalse(file_exists($filename), $message ?: "Failed asserting that $filename does not exist."); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileDoesNotExist($filename, $message = '') + { + static::assertFileNotExists($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileIsReadable($filename, $message = '') + { + static::assertFileExists($filename, $message); + static::assertIsReadable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileNotIsReadable($filename, $message = '') + { + static::assertFileExists($filename, $message); + static::assertNotIsReadable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileIsNotReadable($filename, $message = '') + { + static::assertFileNotIsReadable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileIsWritable($filename, $message = '') + { + static::assertFileExists($filename, $message); + static::assertIsWritable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileNotIsWritable($filename, $message = '') + { + static::assertFileExists($filename, $message); + static::assertNotIsWritable($filename, $message); + } + + /** + * @param string $filename + * @param string $message + * + * @return void + */ + public static function assertFileIsNotWritable($filename, $message = '') + { + static::assertFileNotIsWritable($filename, $message); + } + + /** + * @param string $pattern + * @param string $string + * @param string $message + * + * @return void + */ + public static function assertMatchesRegularExpression($pattern, $string, $message = '') + { + static::assertRegExp($pattern, $string, $message); + } + + /** + * @param string $pattern + * @param string $string + * @param string $message + * + * @return void + */ + public static function assertDoesNotMatchRegularExpression($pattern, $string, $message = '') + { + static::assertNotRegExp($pattern, $string, $message); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.php new file mode 100644 index 0000000000000..ad2150436833d --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/PolyfillTestCaseTrait.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\Bridge\PhpUnit\Legacy; + +use PHPUnit\Framework\Error\Error; +use PHPUnit\Framework\Error\Notice; +use PHPUnit\Framework\Error\Warning; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; + +/** + * This trait is @internal. + */ +trait PolyfillTestCaseTrait +{ + /** + * @param string|string[] $originalClassName + * + * @return MockObject + */ + protected function createMock($originalClassName) + { + $mock = $this->getMockBuilder($originalClassName) + ->disableOriginalConstructor() + ->disableOriginalClone() + ->disableArgumentCloning(); + + if (method_exists($mock, 'disallowMockingUnknownTypes')) { + $mock = $mock->disallowMockingUnknownTypes(); + } + + return $mock->getMock(); + } + + /** + * @param string|string[] $originalClassName + * @param string[] $methods + * + * @return MockObject + */ + protected function createPartialMock($originalClassName, array $methods) + { + $mock = $this->getMockBuilder($originalClassName) + ->disableOriginalConstructor() + ->disableOriginalClone() + ->disableArgumentCloning() + ->setMethods(empty($methods) ? null : $methods); + + if (method_exists($mock, 'disallowMockingUnknownTypes')) { + $mock = $mock->disallowMockingUnknownTypes(); + } + + return $mock->getMock(); + } + + /** + * @param string $exception + * + * @return void + */ + public function expectException($exception) + { + $this->doExpectException($exception); + } + + /** + * @param int|string $code + * + * @return void + */ + public function expectExceptionCode($code) + { + $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionCode'); + $property->setAccessible(true); + $property->setValue($this, $code); + } + + /** + * @param string $message + * + * @return void + */ + public function expectExceptionMessage($message) + { + $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionMessage'); + $property->setAccessible(true); + $property->setValue($this, $message); + } + + /** + * @param string $messageRegExp + * + * @return void + */ + public function expectExceptionMessageMatches($messageRegExp) + { + $this->expectExceptionMessageRegExp($messageRegExp); + } + + /** + * @param string $messageRegExp + * + * @return void + */ + public function expectExceptionMessageRegExp($messageRegExp) + { + $property = new \ReflectionProperty(TestCase::class, 'expectedExceptionMessageRegExp'); + $property->setAccessible(true); + $property->setValue($this, $messageRegExp); + } + + /** + * @return void + */ + public function expectNotice() + { + $this->doExpectException(Notice::class); + } + + /** + * @param string $message + * + * @return void + */ + public function expectNoticeMessage($message) + { + $this->expectExceptionMessage($message); + } + + /** + * @param string $regularExpression + * + * @return void + */ + public function expectNoticeMessageMatches($regularExpression) + { + $this->expectExceptionMessageMatches($regularExpression); + } + + /** + * @return void + */ + public function expectWarning() + { + $this->doExpectException(Warning::class); + } + + /** + * @param string $message + * + * @return void + */ + public function expectWarningMessage($message) + { + $this->expectExceptionMessage($message); + } + + /** + * @param string $regularExpression + * + * @return void + */ + public function expectWarningMessageMatches($regularExpression) + { + $this->expectExceptionMessageMatches($regularExpression); + } + + /** + * @return void + */ + public function expectError() + { + $this->doExpectException(Error::class); + } + + /** + * @param string $message + * + * @return void + */ + public function expectErrorMessage($message) + { + $this->expectExceptionMessage($message); + } + + /** + * @param string $regularExpression + * + * @return void + */ + public function expectErrorMessageMatches($regularExpression) + { + $this->expectExceptionMessageMatches($regularExpression); + } + + private function doExpectException($exception) + { + $property = new \ReflectionProperty(TestCase::class, 'expectedException'); + $property->setAccessible(true); + $property->setValue($this, $exception); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.php new file mode 100644 index 0000000000000..ca29c2ae49ab8 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV5.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\Bridge\PhpUnit\Legacy; + +/** + * @internal + */ +trait SetUpTearDownTraitForV5 +{ + /** + * @return void + */ + public static function setUpBeforeClass() + { + self::doSetUpBeforeClass(); + } + + /** + * @return void + */ + public static function tearDownAfterClass() + { + self::doTearDownAfterClass(); + } + + /** + * @return void + */ + protected function setUp() + { + self::doSetUp(); + } + + /** + * @return void + */ + protected function tearDown() + { + self::doTearDown(); + } + + private static function doSetUpBeforeClass() + { + parent::setUpBeforeClass(); + } + + private static function doTearDownAfterClass() + { + parent::tearDownAfterClass(); + } + + private function doSetUp() + { + parent::setUp(); + } + + private function doTearDown() + { + parent::tearDown(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.php b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.php new file mode 100644 index 0000000000000..cc81df281880a --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SetUpTearDownTraitForV8.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\Bridge\PhpUnit\Legacy; + +/** + * @internal + */ +trait SetUpTearDownTraitForV8 +{ + public static function setUpBeforeClass(): void + { + self::doSetUpBeforeClass(); + } + + public static function tearDownAfterClass(): void + { + self::doTearDownAfterClass(); + } + + protected function setUp(): void + { + self::doSetUp(); + } + + protected function tearDown(): void + { + self::doTearDown(); + } + + private static function doSetUpBeforeClass(): void + { + parent::setUpBeforeClass(); + } + + private static function doTearDownAfterClass(): void + { + parent::tearDownAfterClass(); + } + + private function doSetUp(): void + { + parent::setUp(); + } + + private function doTearDown(): void + { + parent::tearDown(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php index 2da40f2c204f1..9b646dca8dfab 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV5.php @@ -22,7 +22,7 @@ class SymfonyTestsListenerForV5 extends \PHPUnit_Framework_BaseTestListener { private $trait; - public function __construct(array $mockedNamespaces = array()) + public function __construct(array $mockedNamespaces = []) { $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); } @@ -47,11 +47,6 @@ public function startTest(\PHPUnit_Framework_Test $test) $this->trait->startTest($test); } - public function addWarning(\PHPUnit_Framework_Test $test, \PHPUnit_Framework_Warning $e, $time) - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(\PHPUnit_Framework_Test $test, $time) { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php index 5b864bfe58a32..8f2f6b5a7ed54 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV6.php @@ -14,7 +14,6 @@ use PHPUnit\Framework\BaseTestListener; use PHPUnit\Framework\Test; use PHPUnit\Framework\TestSuite; -use PHPUnit\Framework\Warning; /** * Collects and replays skipped tests. @@ -27,7 +26,7 @@ class SymfonyTestsListenerForV6 extends BaseTestListener { private $trait; - public function __construct(array $mockedNamespaces = array()) + public function __construct(array $mockedNamespaces = []) { $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); } @@ -52,11 +51,6 @@ public function startTest(Test $test) $this->trait->startTest($test); } - public function addWarning(Test $test, Warning $e, $time) - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(Test $test, $time) { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php index 18bbdbeba0f03..15f60a453f5be 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerForV7.php @@ -15,7 +15,6 @@ use PHPUnit\Framework\TestListener; use PHPUnit\Framework\TestListenerDefaultImplementation; use PHPUnit\Framework\TestSuite; -use PHPUnit\Framework\Warning; /** * Collects and replays skipped tests. @@ -30,7 +29,7 @@ class SymfonyTestsListenerForV7 implements TestListener private $trait; - public function __construct(array $mockedNamespaces = array()) + public function __construct(array $mockedNamespaces = []) { $this->trait = new SymfonyTestsListenerTrait($mockedNamespaces); } @@ -55,11 +54,6 @@ public function startTest(Test $test): void $this->trait->startTest($test); } - public function addWarning(Test $test, Warning $e, float $time): void - { - $this->trait->addWarning($test, $e, $time); - } - public function endTest(Test $test, float $time): void { $this->trait->endTest($test, $time); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index 803f7114b8129..0f9238bdd9c1c 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -15,7 +15,10 @@ use PHPUnit\Framework\AssertionFailedError; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; +use PHPUnit\Runner\BaseTestRunner; use PHPUnit\Util\Blacklist; +use PHPUnit\Util\ExcludeList; +use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Bridge\PhpUnit\DnsMock; use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; @@ -33,32 +36,34 @@ class SymfonyTestsListenerTrait private static $globallyEnabled = false; private $state = -1; private $skippedFile = false; - private $wasSkipped = array(); - private $isSkipped = array(); - private $expectedDeprecations = array(); - private $gatheredDeprecations = array(); + private $wasSkipped = []; + private $isSkipped = []; + private $expectedDeprecations = []; + private $gatheredDeprecations = []; private $previousErrorHandler; - private $testsWithWarnings; - private $reportUselessTests; private $error; private $runsInSeparateProcess = false; /** * @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive) */ - public function __construct(array $mockedNamespaces = array()) + public function __construct(array $mockedNamespaces = []) { - if (class_exists('PHPUnit_Util_Blacklist')) { - \PHPUnit_Util_Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait'] = 2; + if (class_exists(ExcludeList::class)) { + (new ExcludeList())->getExcludedDirectories(); + ExcludeList::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); + } elseif (method_exists(Blacklist::class, 'addDirectory')) { + (new BlackList())->getBlacklistedDirectories(); + Blacklist::addDirectory(\dirname((new \ReflectionClass(__CLASS__))->getFileName(), 2)); } else { - Blacklist::$blacklistedClassNames['\Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait'] = 2; + Blacklist::$blacklistedClassNames[__CLASS__] = 2; } $enableDebugClassLoader = class_exists(DebugClassLoader::class) || class_exists(LegacyDebugClassLoader::class); foreach ($mockedNamespaces as $type => $namespaces) { if (!\is_array($namespaces)) { - $namespaces = array($namespaces); + $namespaces = [$namespaces]; } if ('time-sensitive' === $type) { foreach ($namespaces as $ns) { @@ -113,19 +118,13 @@ public function globalListenerDisabled() public function startTestSuite($suite) { - if (class_exists('PHPUnit_Util_Blacklist', false)) { - $Test = 'PHPUnit_Util_Test'; - } else { - $Test = 'PHPUnit\Util\Test'; - } $suiteName = $suite->getName(); - $this->testsWithWarnings = array(); foreach ($suite->tests() as $test) { - if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { + if (!($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { continue; } - if (null === $Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) { + if (null === Test::getPreserveGlobalStateSettings(\get_class($test), $test->getName(false))) { $test->setPreserveGlobalState(false); } } @@ -134,8 +133,8 @@ public function startTestSuite($suite) echo "Testing $suiteName\n"; $this->state = 0; - if (!class_exists('Doctrine\Common\Annotations\AnnotationRegistry', false) && class_exists('Doctrine\Common\Annotations\AnnotationRegistry')) { - if (method_exists('Doctrine\Common\Annotations\AnnotationRegistry', 'registerUniqueLoader')) { + if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { + if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { AnnotationRegistry::registerUniqueLoader('class_exists'); } else { AnnotationRegistry::registerLoader('class_exists'); @@ -150,11 +149,11 @@ public function startTestSuite($suite) if (!$this->wasSkipped = require $this->skippedFile) { echo "All tests already ran successfully.\n"; - $suite->setTests(array()); + $suite->setTests([]); } } } - $testSuites = array($suite); + $testSuites = [$suite]; for ($i = 0; isset($testSuites[$i]); ++$i) { foreach ($testSuites[$i]->tests() as $test) { if ($test instanceof \PHPUnit_Framework_TestSuite || $test instanceof TestSuite) { @@ -162,7 +161,7 @@ public function startTestSuite($suite) $testSuites[] = $test; continue; } - $groups = $Test::getGroups($test->getName()); + $groups = Test::getGroups($test->getName()); if (\in_array('time-sensitive', $groups, true)) { ClockMock::register($test->getName()); } @@ -173,12 +172,19 @@ public function startTestSuite($suite) } } } elseif (2 === $this->state) { - $skipped = array(); - foreach ($suite->tests() as $test) { - if (!($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase) - || isset($this->wasSkipped[$suiteName]['*']) - || isset($this->wasSkipped[$suiteName][$test->getName()])) { - $skipped[] = $test; + $suites = [$suite]; + $skipped = []; + while ($s = array_shift($suites)) { + foreach ($s->tests() as $test) { + if ($test instanceof \PHPUnit_Framework_TestSuite || $test instanceof TestSuite) { + $suites[] = $test; + continue; + } + if (($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase) + && isset($this->wasSkipped[\get_class($test)][$test->getName()]) + ) { + $skipped[] = $test; + } } } $suite->setTests($skipped); @@ -188,39 +194,20 @@ public function startTestSuite($suite) public function addSkippedTest($test, \Exception $e, $time) { if (0 < $this->state) { - if ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase) { - $class = \get_class($test); - $method = $test->getName(); - } else { - $class = $test->getName(); - $method = '*'; - } - - $this->isSkipped[$class][$method] = 1; + $this->isSkipped[\get_class($test)][$test->getName()] = 1; } } public function startTest($test) { - if (-2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { - if (null !== $test->getTestResultObject()) { - $this->reportUselessTests = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything(); - } - + if (-2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { // This event is triggered before the test is re-run in isolation if ($this->willBeIsolated($test)) { $this->runsInSeparateProcess = tempnam(sys_get_temp_dir(), 'deprec'); putenv('SYMFONY_DEPRECATIONS_SERIALIZE='.$this->runsInSeparateProcess); } - if (class_exists('PHPUnit_Util_Blacklist', false)) { - $Test = 'PHPUnit_Util_Test'; - $AssertionFailedError = 'PHPUnit_Framework_AssertionFailedError'; - } else { - $Test = 'PHPUnit\Util\Test'; - $AssertionFailedError = 'PHPUnit\Framework\AssertionFailedError'; - } - $groups = $Test::getGroups(\get_class($test), $test->getName(false)); + $groups = Test::getGroups(\get_class($test), $test->getName(false)); if (!$this->runsInSeparateProcess) { if (\in_array('time-sensitive', $groups, true)) { @@ -232,50 +219,36 @@ public function startTest($test) } } - $annotations = $Test::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); + if (!$test->getTestResultObject()) { + return; + } + + $annotations = Test::parseTestMethodAnnotations(\get_class($test), $test->getName(false)); if (isset($annotations['class']['expectedDeprecation'])) { - $test->getTestResultObject()->addError($test, new $AssertionFailedError('`@expectedDeprecation` annotations are not allowed at the class level.'), 0); + $test->getTestResultObject()->addError($test, new AssertionFailedError('`@expectedDeprecation` annotations are not allowed at the class level.'), 0); } if (isset($annotations['method']['expectedDeprecation'])) { if (!\in_array('legacy', $groups, true)) { - $this->error = new $AssertionFailedError('Only tests with the `@group legacy` annotation can have `@expectedDeprecation`.'); + $this->error = new AssertionFailedError('Only tests with the `@group legacy` annotation can have `@expectedDeprecation`.'); } $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); $this->expectedDeprecations = $annotations['method']['expectedDeprecation']; - $this->previousErrorHandler = set_error_handler(array($this, 'handleError')); + $this->previousErrorHandler = set_error_handler([$this, 'handleError']); } } } - public function addWarning($test, $e, $time) - { - if ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase) { - $this->testsWithWarnings[$test->getName()] = true; - } - } - public function endTest($test, $time) { - if (class_exists('PHPUnit_Util_Blacklist', false)) { - $Test = 'PHPUnit_Util_Test'; - $BaseTestRunner = 'PHPUnit_Runner_BaseTestRunner'; - $Warning = 'PHPUnit_Framework_Warning'; - } else { - $Test = 'PHPUnit\Util\Test'; - $BaseTestRunner = 'PHPUnit\Runner\BaseTestRunner'; - $Warning = 'PHPUnit\Framework\Warning'; + if (class_exists(DebugClassLoader::class, false)) { + DebugClassLoader::checkClasses(); } - $className = \get_class($test); - $classGroups = $Test::getGroups($className); - $groups = $Test::getGroups($className, $test->getName(false)); - if (null !== $this->reportUselessTests) { - $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything($this->reportUselessTests); - $this->reportUselessTests = null; - } + $className = \get_class($test); + $groups = Test::getGroups($className, $test->getName(false)); if ($errored = null !== $this->error) { $test->getTestResultObject()->addError($test, $this->error, 0); @@ -286,52 +259,50 @@ public function endTest($test, $time) $deprecations = file_get_contents($this->runsInSeparateProcess); unlink($this->runsInSeparateProcess); putenv('SYMFONY_DEPRECATIONS_SERIALIZE'); - foreach ($deprecations ? unserialize($deprecations) : array() as $deprecation) { - $error = serialize(array('deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => isset($deprecation[2]) ? $deprecation[2] : null)); + foreach ($deprecations ? unserialize($deprecations) : [] as $deprecation) { + $error = serialize(['deprecation' => $deprecation[1], 'class' => $className, 'method' => $test->getName(false), 'triggering_file' => isset($deprecation[2]) ? $deprecation[2] : null, 'files_stack' => isset($deprecation[3]) ? $deprecation[3] : []]); if ($deprecation[0]) { // unsilenced on purpose - trigger_error($error, E_USER_DEPRECATED); + trigger_error($error, \E_USER_DEPRECATED); } else { - @trigger_error($error, E_USER_DEPRECATED); + @trigger_error($error, \E_USER_DEPRECATED); } } $this->runsInSeparateProcess = false; } if ($this->expectedDeprecations) { - if (!\in_array($test->getStatus(), array($BaseTestRunner::STATUS_SKIPPED, $BaseTestRunner::STATUS_INCOMPLETE), true)) { + if (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) { $test->addToAssertionCount(\count($this->expectedDeprecations)); } restore_error_handler(); - if (!$errored && !\in_array($test->getStatus(), array($BaseTestRunner::STATUS_SKIPPED, $BaseTestRunner::STATUS_INCOMPLETE, $BaseTestRunner::STATUS_FAILURE, $BaseTestRunner::STATUS_ERROR), true)) { + if (!$errored && !\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { try { $prefix = "@expectedDeprecation:\n"; $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", $this->expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", $this->gatheredDeprecations)."\n"); } catch (AssertionFailedError $e) { $test->getTestResultObject()->addFailure($test, $e, $time); - } catch (\PHPUnit_Framework_AssertionFailedError $e) { - $test->getTestResultObject()->addFailure($test, $e, $time); } } - $this->expectedDeprecations = $this->gatheredDeprecations = array(); + $this->expectedDeprecations = $this->gatheredDeprecations = []; $this->previousErrorHandler = null; } - if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { + if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit_Framework_TestCase || $test instanceof TestCase)) { if (\in_array('time-sensitive', $groups, true)) { ClockMock::withClockMock(false); } if (\in_array('dns-sensitive', $groups, true)) { - DnsMock::withMockedHosts(array()); + DnsMock::withMockedHosts([]); } } } - public function handleError($type, $msg, $file, $line, $context = array()) + public function handleError($type, $msg, $file, $line, $context = []) { - if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { + if (\E_USER_DEPRECATED !== $type && \E_DEPRECATED !== $type) { $h = $this->previousErrorHandler; return $h ? $h($type, $msg, $file, $line, $context) : false; @@ -342,10 +313,12 @@ public function handleError($type, $msg, $file, $line, $context = array()) if (\is_array($parsedMsg)) { $msg = $parsedMsg['deprecation']; } - if (error_reporting()) { + if (error_reporting() & $type) { $msg = 'Unsilenced deprecation: '.$msg; } $this->gatheredDeprecations[] = $msg; + + return null; } /** diff --git a/src/Symfony/Bridge/PhpUnit/README.md b/src/Symfony/Bridge/PhpUnit/README.md index 8c4e6e59ccb47..b7c041a8ee5a7 100644 --- a/src/Symfony/Bridge/PhpUnit/README.md +++ b/src/Symfony/Bridge/PhpUnit/README.md @@ -1,13 +1,14 @@ PHPUnit Bridge ============== -Provides utilities for PHPUnit, especially user deprecation notices management. +The PHPUnit bridge provides utilities for [PHPUnit](https://phpunit.de/), +especially user deprecation notices management. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/phpunit_bridge.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) + * [Documentation](https://symfony.com/doc/current/components/phpunit_bridge.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/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php b/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.php new file mode 100644 index 0000000000000..e27c3a4fb0934 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/SetUpTearDownTrait.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\Bridge\PhpUnit; + +use PHPUnit\Framework\TestCase; + +// A trait to provide forward compatibility with newest PHPUnit versions +$r = new \ReflectionClass(TestCase::class); +if (\PHP_VERSION_ID < 70000 || !$r->getMethod('setUp')->hasReturnType()) { + trait SetUpTearDownTrait + { + use Legacy\SetUpTearDownTraitForV5; + } +} else { + trait SetUpTearDownTrait + { + use Legacy\SetUpTearDownTraitForV8; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php index a753525b76eed..d3cd7563bd41f 100644 --- a/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php +++ b/src/Symfony/Bridge/PhpUnit/SymfonyTestsListener.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\PhpUnit; -if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { +if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV5', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); } elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV6', 'Symfony\Bridge\PhpUnit\SymfonyTestsListener'); diff --git a/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.php b/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.php new file mode 100644 index 0000000000000..d1811575087df --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/BootstrapTest.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\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; + +class BootstrapTest extends TestCase +{ + /** + * @requires PHPUnit < 6.0 + */ + public function testAliasingOfErrorClasses() + { + $this->assertInstanceOf( + \PHPUnit_Framework_Error::class, + new \PHPUnit\Framework\Error\Error('message', 0, __FILE__, __LINE__) + ); + $this->assertInstanceOf( + \PHPUnit_Framework_Error_Deprecated::class, + new \PHPUnit\Framework\Error\Deprecated('message', 0, __FILE__, __LINE__) + ); + $this->assertInstanceOf( + \PHPUnit_Framework_Error_Notice::class, + new \PHPUnit\Framework\Error\Notice('message', 0, __FILE__, __LINE__) + ); + $this->assertInstanceOf( + \PHPUnit_Framework_Error_Warning::class, + new \PHPUnit\Framework\Error\Warning('message', 0, __FILE__, __LINE__) + ); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ClassExistsMockTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ClassExistsMockTest.php index 002d313a6fa01..3e3d5771b1b10 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/ClassExistsMockTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/ClassExistsMockTest.php @@ -16,12 +16,12 @@ class ClassExistsMockTest extends TestCase { - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { ClassExistsMock::register(__CLASS__); } - protected function setUp() + protected function setUp(): void { ClassExistsMock::withMockedClasses([ ExistingClass::class => false, diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ClockMockTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ClockMockTest.php index 5b92ccd8507e4..5af0617ba5a7f 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/ClockMockTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/ClockMockTest.php @@ -21,12 +21,12 @@ */ class ClockMockTest extends TestCase { - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { ClockMock::register(__CLASS__); } - protected function setUp() + protected function setUp(): void { ClockMock::withClockMock(1234567890.125); } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php index b7ba8b0d3d9b0..d6248b5f0b7da 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/CoverageListenerTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\PhpUnit\Tests; use PHPUnit\Framework\TestCase; @@ -27,16 +36,22 @@ public function test() $dir = __DIR__.'/../Tests/Fixtures/coverage'; $phpunit = $_SERVER['argv'][0]; - exec("$php $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text 2> /dev/null", $output); + exec("$php $phpunit -c $dir/phpunit-without-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); - $this->assertContains('FooCov', $output); + $this->assertMatchesRegularExpression('/FooCov\n\s*Methods:\s+100.00%[^\n]+Lines:\s+100.00%/', $output); - exec("$php $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text 2> /dev/null", $output); + exec("$php $phpunit -c $dir/phpunit-with-listener.xml.dist $dir/tests/ --coverage-text --colors=never 2> /dev/null", $output); $output = implode("\n", $output); - $this->assertNotContains('FooCov', $output); - $this->assertContains("SutNotFoundTest::test\nCould not find the tested class.", $output); - $this->assertNotContains("CoversTest::test\nCould not find the tested class.", $output); - $this->assertNotContains("CoversDefaultClassTest::test\nCould not find the tested class.", $output); - $this->assertNotContains("CoversNothingTest::test\nCould not find the tested class.", $output); + + if (false === strpos($output, 'FooCov')) { + $this->addToAssertionCount(1); + } else { + $this->assertMatchesRegularExpression('/FooCov\n\s*Methods:\s+0.00%[^\n]+Lines:\s+0.00%/', $output); + } + + $this->assertStringContainsString("SutNotFoundTest::test\nCould not find the tested class.", $output); + $this->assertStringNotContainsString("CoversTest::test\nCould not find the tested class.", $output); + $this->assertStringNotContainsString("CoversDefaultClassTest::test\nCould not find the tested class.", $output); + $this->assertStringNotContainsString("CoversNothingTest::test\nCould not find the tested class.", $output); } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php index 0c8708bb35f00..9cb0a0e32ce3a 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationTest.php @@ -12,10 +12,40 @@ namespace Symfony\Bridge\PhpUnit\Tests\DeprecationErrorHandler; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerForV5; class DeprecationTest extends TestCase { + private static $vendorDir; + private static $prefixDirsPsr4; + + private static function getVendorDir() + { + if (null !== self::$vendorDir) { + return self::$vendorDir; + } + + foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $vendorDir = \dirname(\dirname($r->getFileName())); + if (file_exists($vendorDir.'/composer/installed.json') && @mkdir($vendorDir.'/myfakevendor/myfakepackage1', 0777, true)) { + break; + } + } + } + + self::$vendorDir = $vendorDir; + @mkdir($vendorDir.'/myfakevendor/myfakepackage2'); + touch($vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile1.php'); + touch($vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile2.php'); + touch($vendorDir.'/myfakevendor/myfakepackage2/MyFakeFile.php'); + + return self::$vendorDir; + } + public function testItCanDetermineTheClassWhereTheDeprecationHappened() { $deprecation = new Deprecation('πŸ’©', $this->debugBacktrace(), __FILE__); @@ -28,7 +58,7 @@ public function testItCanTellWhetherItIsInternal() { $r = new \ReflectionClass(Deprecation::class); - if (dirname($r->getFileName(), 2) !== dirname(__DIR__, 2)) { + if (\dirname(\dirname($r->getFileName())) !== \dirname(\dirname(__DIR__))) { $this->markTestSkipped('Test case is not compatible with having the bridge in vendor/'); } @@ -45,8 +75,8 @@ public function testLegacyTestMethodIsDetectedAsSuch() public function testItCanBeConvertedToAString() { $deprecation = new Deprecation('πŸ’©', $this->debugBacktrace(), __FILE__); - $this->assertContains('πŸ’©', $deprecation->toString()); - $this->assertContains(__FUNCTION__, $deprecation->toString()); + $this->assertStringContainsString('πŸ’©', $deprecation->toString()); + $this->assertStringContainsString(__FUNCTION__, $deprecation->toString()); } public function testItRulesOutFilesOutsideVendorsAsIndirect() @@ -55,12 +85,212 @@ public function testItRulesOutFilesOutsideVendorsAsIndirect() $this->assertNotSame(Deprecation::TYPE_INDIRECT, $deprecation->getType()); } + /** + * @dataProvider mutedProvider + */ + public function testItMutesOnlySpecificErrorMessagesWhenTheCallingCodeIsInPhpunit($muted, $callingClass, $message) + { + $trace = $this->debugBacktrace(); + array_unshift($trace, ['class' => $callingClass]); + array_unshift($trace, ['class' => DeprecationErrorHandler::class]); + $deprecation = new Deprecation($message, $trace, 'should_not_matter.php'); + $this->assertSame($muted, $deprecation->isMuted()); + } + + public function mutedProvider() + { + yield 'not from phpunit, and not a whitelisted message' => [ + false, + \My\Source\Code::class, + 'Self deprecating humor is deprecated by itself', + ]; + yield 'from phpunit, but not a whitelisted message' => [ + false, + \PHPUnit\Random\Piece\Of\Code::class, + 'Self deprecating humor is deprecated by itself', + ]; + yield 'whitelisted message, but not from phpunit' => [ + false, + \My\Source\Code::class, + 'Function ReflectionType::__toString() is deprecated', + ]; + yield 'from phpunit and whitelisted message' => [ + true, + \PHPUnit\Random\Piece\Of\Code::class, + 'Function ReflectionType::__toString() is deprecated', + ]; + } + + public function testNotMutedIfNotCalledFromAClassButARandomFile() + { + $deprecation = new Deprecation( + 'Function ReflectionType::__toString() is deprecated', + [ + ['file' => 'should_not_matter.php'], + ['file' => 'should_not_matter_either.php'], + ], + 'my-procedural-controller.php' + ); + $this->assertFalse($deprecation->isMuted()); + } + + public function testItTakesMutesDeprecationFromPhpUnitFiles() + { + $deprecation = new Deprecation( + 'Function ReflectionType::__toString() is deprecated', + [ + ['file' => 'should_not_matter.php'], + ['file' => 'should_not_matter_either.php'], + ], + 'random_path'.\DIRECTORY_SEPARATOR.'vendor'.\DIRECTORY_SEPARATOR.'phpunit'.\DIRECTORY_SEPARATOR.'whatever.php' + ); + $this->assertTrue($deprecation->isMuted()); + } + + public function providerGetTypeDetectsSelf() + { + return [ + 'not_from_vendors_file' => [Deprecation::TYPE_SELF, '', 'MyClass1', __FILE__], + 'nonexistent_file' => [Deprecation::TYPE_UNDETERMINED, '', 'MyClass1', 'dummy_vendor_path'], + 'serialized_trace_with_nonexistent_triggering_file' => [ + Deprecation::TYPE_UNDETERMINED, + serialize([ + 'class' => '', + 'method' => '', + 'deprecation' => '', + 'triggering_file' => 'dummy_vendor_path', + 'files_stack' => [], + ]), + SymfonyTestsListenerForV5::class, + '', + ], + ]; + } + + /** + * @dataProvider providerGetTypeDetectsSelf + */ + public function testGetTypeDetectsSelf(string $expectedType, string $message, string $traceClass, string $file) + { + $trace = [ + ['class' => 'MyClass1', 'function' => 'myMethod'], + ['function' => 'trigger_error'], + ['class' => SymfonyTestsListenerTrait::class, 'function' => 'endTest'], + ['class' => $traceClass, 'function' => 'myMethod'], + ]; + $deprecation = new Deprecation($message, $trace, $file); + $this->assertSame($expectedType, $deprecation->getType()); + } + + public function providerGetTypeUsesRightTrace() + { + $vendorDir = self::getVendorDir(); + $fakeTrace = [ + ['function' => 'trigger_error'], + ['class' => SymfonyTestsListenerTrait::class, 'function' => 'endTest'], + ['class' => SymfonyTestsListenerForV5::class, 'function' => 'endTest'], + ]; + + return [ + 'no_file_in_stack' => [Deprecation::TYPE_DIRECT, '', [['function' => 'myfunc1'], ['function' => 'myfunc2']]], + 'files_in_stack_from_various_packages' => [ + Deprecation::TYPE_INDIRECT, + '', + [ + ['function' => 'myfunc1', 'file' => $vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile1.php'], + ['function' => 'myfunc2', 'file' => $vendorDir.'/myfakevendor/myfakepackage2/MyFakeFile.php'], + ], + ], + 'serialized_stack_files_from_same_package' => [ + Deprecation::TYPE_DIRECT, + serialize([ + 'deprecation' => 'My deprecation message', + 'class' => 'MyClass', + 'method' => 'myMethod', + 'files_stack' => [ + $vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile1.php', + $vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile2.php', + ], + ]), + $fakeTrace, + ], + 'serialized_stack_files_from_various_packages' => [ + Deprecation::TYPE_INDIRECT, + serialize([ + 'deprecation' => 'My deprecation message', + 'class' => 'MyClass', + 'method' => 'myMethod', + 'files_stack' => [ + $vendorDir.'/myfakevendor/myfakepackage1/MyFakeFile1.php', + $vendorDir.'/myfakevendor/myfakepackage2/MyFakeFile.php', + ], + ]), + $fakeTrace, + ], + ]; + } + + /** + * @dataProvider providerGetTypeUsesRightTrace + */ + public function testGetTypeUsesRightTrace(string $expectedType, string $message, array $trace) + { + $deprecation = new Deprecation( + $message, + $trace, + self::getVendorDir().'/myfakevendor/myfakepackage2/MyFakeFile.php' + ); + $this->assertSame($expectedType, $deprecation->getType()); + } + /** * This method is here to simulate the extra level from the piece of code - * triggering an error to the error handler + * triggering an error to the error handler. */ - public function debugBacktrace(): array + public function debugBacktrace() { return debug_backtrace(); } + + private static function removeDir($dir) + { + $files = glob($dir.'/*'); + foreach ($files as $file) { + if (is_file($file)) { + unlink($file); + } else { + self::removeDir($file); + } + } + rmdir($dir); + } + + public static function setupBeforeClass(): void + { + 10000 foreach (get_declared_classes() as $class) { + if ('C' === $class[0] && 0 === strpos($class, 'ComposerAutoloaderInit')) { + $r = new \ReflectionClass($class); + $v = \dirname(\dirname($r->getFileName())); + if (file_exists($v.'/composer/installed.json')) { + $loader = require $v.'/autoload.php'; + $reflection = new \ReflectionClass($loader); + $prop = $reflection->getProperty('prefixDirsPsr4'); + $prop->setAccessible(true); + $currentValue = $prop->getValue($loader); + self::$prefixDirsPsr4[] = [$prop, $loader, $currentValue]; + $currentValue['Symfony\\Bridge\\PhpUnit\\'] = [realpath(__DIR__.'/../..')]; + $prop->setValue($loader, $currentValue); + } + } + } + } + + public static function tearDownAfterClass(): void + { + foreach (self::$prefixDirsPsr4 as [$prop, $loader, $prefixDirsPsr4]) { + $prop->setValue($loader, $prefixDirsPsr4); + } + + self::removeDir(self::getVendorDir().'/myfakevendor'); + } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt new file mode 100644 index 0000000000000..04e64d33e46b6 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_autoload.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test that a deprecation from the DebugClassLoader on a vendor class autoload triggered by an app class is considered indirect. +--FILE-- + +--EXPECTF-- +Remaining indirect deprecation notices (1) + + 1x: The "acme\lib\ExtendsDeprecatedClassFromOtherVendor" class extends "fcy\lib\DeprecatedClass" that is deprecated. + 1x in BarService::__construct from App\Services diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt new file mode 100644 index 0000000000000..a6b0133af93ed --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/debug_class_loader_deprecation.phpt @@ -0,0 +1,41 @@ +--TEST-- +Test that a deprecation from the DebugClassLoader triggered by an app class extending a vendor one is considered direct. +--FILE-- + +--EXPECTF-- +Remaining direct deprecation notices (1) + + 1x: The "App\Services\ExtendsDeprecatedFromVendor" class extends "fcy\lib\DeprecatedClass" that is deprecated. + 1x in DebugClassLoader::loadClass from Symfony\Component\ErrorHandler diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/deprecation/deprecation.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/deprecation/deprecation.php index b9e23e7692156..92efd9500f973 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/deprecation/deprecation.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/deprecation/deprecation.php @@ -1,3 +1,3 @@ deprecatedApi(); + + $service2 = new SomeOtherService(); + $service2->deprecatedApi(); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php new file mode 100644 index 0000000000000..868de5bd443db --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_app/BarService.php @@ -0,0 +1,13 @@ + [__DIR__.'/../../fake_app/'], + 'acme\\lib\\' => [__DIR__.'/../acme/lib/'], + 'fcy\\lib\\' => [__DIR__.'/../fcy/lib/'], + ]; + } + + public function loadClass($className) + { + if ($file = $this->findFile($className)) { + require $file; + } + } + + public function findFile($class) + { + foreach ($this->getPrefixesPsr4() as $prefix => $baseDirs) { + if (0 !== strpos($class, $prefix)) { + continue; + } + + foreach ($baseDirs as $baseDir) { + $file = str_replace([$prefix, '\\'], [$baseDir, '/'], $class.'.php'); + if (file_exists($file)) { + return $file; + } + } + } + + return false; } } class ComposerAutoloaderInitFake { + private static $loader; + public static function getLoader() { - return new ComposerLoaderFake(); + if (null === self::$loader) { + self::$loader = new ComposerLoaderFake(); + spl_autoload_register([self::$loader, 'loadClass']); + } + + return self::$loader; } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php new file mode 100644 index 0000000000000..f6672cea20400 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor/fcy/lib/DeprecatedClass.php @@ -0,0 +1,10 @@ + [__DIR__.'/../foo/lib/'], + ]; + } + + public function loadClass($className) + { + foreach ($this->getPrefixesPsr4() as $prefix => $baseDirs) { + if (0 !== strpos($className, $prefix)) { + continue; + } + + foreach ($baseDirs as $baseDir) { + $file = str_replace([$prefix, '\\'], [$baseDir, '/'], $className.'.php'); + if (file_exists($file)) { + require $file; + } + } + } + } +} + +class ComposerAutoloaderInitFakeBis +{ + private static $loader; + + public static function getLoader() + { + if (null === self::$loader) { + self::$loader = new ComposerLoaderFakeBis(); + spl_autoload_register([self::$loader, 'loadClass']); + } + + return self::$loader; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/installed.json b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/installed.json new file mode 100644 index 0000000000000..bf4fecc9fbf79 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/composer/installed.json @@ -0,0 +1 @@ +{"just here": "for the detection"} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php new file mode 100644 index 0000000000000..8ab3230724c06 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/fake_vendor_bis/foo/lib/SomeOtherService.php @@ -0,0 +1,14 @@ +buildFromDirectory(__DIR__.DIRECTORY_SEPARATOR.'deprecation'); +$phar = new Phar(__DIR__.\DIRECTORY_SEPARATOR.'deprecation.phar', 0, 'deprecation.phar'); +$phar->buildFromDirectory(__DIR__.\DIRECTORY_SEPARATOR.'deprecation'); diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt new file mode 100644 index 0000000000000..336001f500113 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/multiple_autoloads.phpt @@ -0,0 +1,45 @@ +--TEST-- +Test DeprecationErrorHandler with multiple autoload files +--FILE-- +directDeprecations(); +?> +--EXPECTF-- +Remaining direct deprecation notices (2) + + 1x: deprecatedApi is deprecated! You should stop relying on it! + 1x in AppService::directDeprecations from App\Services + + 1x: deprecatedApi from foo is deprecated! You should stop relying on it! + 1x in AppService::directDeprecations from App\Services diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/php_deprecation_from_vendor_class.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/php_deprecation_from_vendor_class.phpt new file mode 100644 index 0000000000000..1ead2ef4a4013 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/php_deprecation_from_vendor_class.phpt @@ -0,0 +1,43 @@ +--TEST-- +Test that a PHP deprecation from a vendor class autoload is considered indirect. +--SKIPIF-- + +--FILE-- + +--EXPECTF-- +Remaining indirect deprecation notices (1) + + 1x: acme\lib\PhpDeprecation implements the Serializable interface, which is deprecated. Implement __serialize() and __unserialize() instead (or in addition, if support for old PHP versions is necessary) + 1x in DebugClassLoader::loadClass from Symfony\Component\ErrorHandler diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet.phpt index 91cee8e17fa95..5b6e325106f03 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet.phpt +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet.phpt @@ -4,7 +4,7 @@ Test DeprecationErrorHandler with quiet output array(array('type' => 'MX')))); + DnsMock::withMockedHosts(['example.com' => [['type' => 'MX']]]); $this->assertTrue(DnsMock::checkdnsrr('example.com')); - DnsMock::withMockedHosts(array('example.com' => array(array('type' => 'A')))); + DnsMock::withMockedHosts(['example.com' => [['type' => 'A']]]); $this->assertFalse(DnsMock::checkdnsrr('example.com')); $this->assertTrue(DnsMock::checkdnsrr('example.com', 'a')); $this->assertTrue(DnsMock::checkdnsrr('example.com', 'any')); @@ -35,34 +35,34 @@ public function testCheckdnsrr() public function testGetmxrr() { - DnsMock::withMockedHosts(array( - 'example.com' => array(array( + DnsMock::withMockedHosts([ + 'example.com' => [[ 'type' => 'MX', 'host' => 'mx.example.com', 'pri' => 10, - )), - )); + ]], + ]); $this->assertFalse(DnsMock::getmxrr('foobar.com', $mxhosts, $weight)); $this->assertTrue(DnsMock::getmxrr('example.com', $mxhosts, $weight)); - $this->assertSame(array('mx.example.com'), $mxhosts); - $this->assertSame(array(10), $weight); + $this->assertSame(['mx.example.com'], $mxhosts); + $this->assertSame([10], $weight); } public function testGethostbyaddr() { - DnsMock::withMockedHosts(array( - 'example.com' => array( - array( + DnsMock::withMockedHosts([ + 'example.com' => [ + [ 'type' => 'A', 'ip' => '1.2.3.4', - ), - array( + ], + [ 'type' => 'AAAA', 'ipv6' => '::12', - ), - ), - )); + ], + ], + ]); $this->assertSame('::21', DnsMock::gethostbyaddr('::21')); $this->assertSame('example.com', DnsMock::gethostbyaddr('::12')); @@ -71,18 +71,18 @@ public function testGethostbyaddr() public function testGethostbyname() { - DnsMock::withMockedHosts(array( - 'example.com' => array( - array( + DnsMock::withMockedHosts([ + 'example.com' => [ + [ 'type' => 'AAAA', 'ipv6' => '::12', - ), - array( + ], + [ 'type' => 'A', 'ip' => '1.2.3.4', - ), - ), - )); + ], + ], + ]); $this->assertSame('foobar.com', DnsMock::gethostbyname('foobar.com')); $this->assertSame('1.2.3.4', DnsMock::gethostbyname('example.com')); @@ -90,59 +90,59 @@ public function testGethostbyname() public function testGethostbynamel() { - DnsMock::withMockedHosts(array( - 'example.com' => array( - array( + DnsMock::withMockedHosts([ + 'example.com' => [ + [ 'type' => 'A', 'ip' => '1.2.3.4', - ), - array( + ], + [ 'type' => 'A', 'ip' => '2.3.4.5', - ), - ), - )); + ], + ], + ]); $this->assertFalse(DnsMock::gethostbynamel('foobar.com')); - $this->assertSame(array('1.2.3.4', '2.3.4.5'), DnsMock::gethostbynamel('example.com')); + $this->assertSame(['1.2.3.4', '2.3.4.5'], DnsMock::gethostbynamel('example.com')); } public function testDnsGetRecord() { - DnsMock::withMockedHosts(array( - 'example.com' => array( - array( + DnsMock::withMockedHosts([ + 'example.com' => [ + [ 'type' => 'A', 'ip' => '1.2.3.4', - ), - array( + ], + [ 'type' => 'PTR', 'ip' => '2.3.4.5', - ), - ), - )); + ], + ], + ]); - $records = array( - array( + $records = [ + [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 1, 'type' => 'A', 'ip' => '1.2.3.4', - ), - $ptr = array( + ], + $ptr = [ 'host' => 'example.com', 'class' => 'IN', 'ttl' => 1, 'type' => 'PTR', 'ip' => '2.3.4.5', - ), - ); + ], + ]; $this->assertFalse(DnsMock::dns_get_record('foobar.com')); $this->assertSame($records, DnsMock::dns_get_record('example.com')); - $this->assertSame($records, DnsMock::dns_get_record('example.com', DNS_ALL)); - $this->assertSame($records, DnsMock::dns_get_record('example.com', DNS_A | DNS_PTR)); - $this->assertSame(array($ptr), DnsMock::dns_get_record('example.com', DNS_PTR)); + $this->assertSame($records, DnsMock::dns_get_record('example.com', \DNS_ALL)); + $this->assertSame($records, DnsMock::dns_get_record('example.com', \DNS_A | \DNS_PTR)); + $this->assertSame([$ptr], DnsMock::dns_get_record('example.com', \DNS_PTR)); } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.php new file mode 100644 index 0000000000000..329bf694d295f --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/ExpectedDeprecationAnnotationTest.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\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; + +final class ExpectedDeprecationAnnotationTest extends TestCase +{ + /** + * Do not remove this test in the next major versions. + * + * @group legacy + * + * @expectedDeprecation foo + */ + public function testOne() + { + @trigger_error('foo', \E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major versions. + * + * @group legacy + * + * @expectedDeprecation foo + * @expectedDeprecation bar + */ + public function testMany() + { + @trigger_error('foo', \E_USER_DEPRECATED); + @trigger_error('bar', \E_USER_DEPRECATED); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarCovTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarCovTest.php index dc7f2cefd01c3..d0280dc13a1dc 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarCovTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/BarCovTest.php @@ -17,7 +17,7 @@ class BarCovTest extends TestCase { public function testBarCov() { - if (!class_exists('PhpUnitCoverageTest\FooCov')) { + if (!class_exists(\PhpUnitCoverageTest\FooCov::class)) { $this->markTestSkipped('This test is not part of the main Symfony test suite. It\'s here to test the CoverageListener.'); } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFindTest.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFoundTest.php similarity index 100% rename from src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFindTest.php rename to src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/SutNotFoundTest.php diff --git a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php index 241006431acea..3e45381dce2a0 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/Fixtures/coverage/tests/bootstrap.php @@ -14,7 +14,7 @@ require __DIR__.'/../../../../Legacy/CoverageListenerTrait.php'; -if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { +if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { require_once __DIR__.'/../../../../Legacy/CoverageListenerForV5.php'; } elseif (version_compare(\PHPUnit\Runner\Version::id(), '7.0.0', '<')) { require_once __DIR__.'/../../../../Legacy/CoverageListenerForV6.php'; diff --git a/src/Symfony/Bridge/PhpUnit/Tests/OnlyExpectingDeprecationSkippedTest.php b/src/Symfony/Bridge/PhpUnit/Tests/OnlyExpectingDeprecationSkippedTest.php new file mode 100644 index 0000000000000..593e0b4e14342 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/OnlyExpectingDeprecationSkippedTest.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\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; + +/** + * This test is meant to be skipped. + * + * @requires extension ext-dummy + */ +final class OnlyExpectingDeprecationSkippedTest extends TestCase +{ + /** + * Do not remove this test in the next major versions. + * + * @group legacy + * + * @expectedDeprecation unreachable + */ + public function testExpectingOnlyDeprecations() + { + $this->fail('should never be ran.'); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php index ec8f124a5f2c1..04bf6ec80776a 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/ProcessIsolationTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\PhpUnit\Tests; use PHPUnit\Framework\TestCase; @@ -18,20 +27,15 @@ class ProcessIsolationTest extends TestCase */ public function testIsolation() { - @trigger_error('Test abc', E_USER_DEPRECATED); + @trigger_error('Test abc', \E_USER_DEPRECATED); $this->addToAssertionCount(1); } public function testCallingOtherErrorHandler() { - $class = class_exists('PHPUnit\Framework\Exception') ? 'PHPUnit\Framework\Exception' : 'PHPUnit_Framework_Exception'; - if (method_exists($this, 'expectException')) { - $this->expectException($class); - $this->expectExceptionMessage('Test that PHPUnit\'s error handler fires.'); - } else { - $this->setExpectedException($class, 'Test that PHPUnit\'s error handler fires.'); - } + $this->expectException(\PHPUnit\Framework\Exception::class); + $this->expectExceptionMessage('Test that PHPUnit\'s error handler fires.'); - trigger_error('Test that PHPUnit\'s error handler fires.', E_USER_WARNING); + trigger_error('Test that PHPUnit\'s error handler fires.', \E_USER_WARNING); } } diff --git a/src/Symfony/Bridge/PhpUnit/TextUI/Command.php b/src/Symfony/Bridge/PhpUnit/TextUI/Command.php index 4a26fc7fad278..8690812b56b57 100644 --- a/src/Symfony/Bridge/PhpUnit/TextUI/Command.php +++ b/src/Symfony/Bridge/PhpUnit/TextUI/Command.php @@ -11,10 +11,12 @@ namespace Symfony\Bridge\PhpUnit\TextUI; -if (class_exists('PHPUnit_Runner_Version') && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { +if (version_compare(\PHPUnit\Runner\Version::id(), '6.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV5', 'Symfony\Bridge\PhpUnit\TextUI\Command'); -} else { +} elseif (version_compare(\PHPUnit\Runner\Version::id(), '9.0.0', '<')) { class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV6', 'Symfony\Bridge\PhpUnit\TextUI\Command'); +} else { + class_alias('Symfony\Bridge\PhpUnit\Legacy\CommandForV9', 'Symfony\Bridge\PhpUnit\TextUI\Command'); } if (false) { diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 247aff665fed0..ca79f45ac9985 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -10,11 +10,18 @@ */ // Please update when phpunit needs to be reinstalled with fresh deps: -// Cache-Id-Version: 2019-07-04 18:00 UTC +// Cache-Id: 2021-02-04 11:00 UTC + +if ('cli' !== \PHP_SAPI && 'phpdbg' !== \PHP_SAPI) { + throw new Exception('This script must be run from the command line.'); +} error_reporting(-1); -$getEnvVar = function ($name, $default = false) { +global $argv, $argc; +$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : []; +$argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0; +$getEnvVar = function ($name, $default = false) use ($argv) { if (false !== $value = getenv($name)) { return $value; } @@ -22,20 +29,55 @@ static $phpunitConfig = null; if (null === $phpunitConfig) { $phpunitConfigFilename = null; - if (file_exists('phpunit.xml')) { - $phpunitConfigFilename = 'phpunit.xml'; - } elseif (file_exists('phpunit.xml.dist')) { - $phpunitConfigFilename = 'phpunit.xml.dist'; + $getPhpUnitConfig = function ($probableConfig) use (&$getPhpUnitConfig) { + if (!$probableConfig) { + return null; + } + if (is_dir($probableConfig)) { + return $getPhpUnitConfig($probableConfig.\DIRECTORY_SEPARATOR.'phpunit.xml'); + } + + if (file_exists($probableConfig)) { + return $probableConfig; + } + if (file_exists($probableConfig.'.dist')) { + return $probableConfig.'.dist'; + } + + return null; + }; + + foreach ($argv as $cliArgumentIndex => $cliArgument) { + if ('--' === $cliArgument) { + break; + } + // long option + if ('--configuration' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) { + $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]); + break; + } + // short option + if (0 === strpos($cliArgument, '-c')) { + if ('-c' === $cliArgument && array_key_exists($cliArgumentIndex + 1, $argv)) { + $phpunitConfigFilename = $getPhpUnitConfig($argv[$cliArgumentIndex + 1]); + } else { + $phpunitConfigFilename = $getPhpUnitConfig(substr($cliArgument, 2)); + } + break; + } } + + $phpunitConfigFilename = $phpunitConfigFilename ?: $getPhpUnitConfig('phpunit.xml'); + if ($phpunitConfigFilename) { - $phpunitConfig = new DomDocument(); + $phpunitConfig = new DOMDocument(); $phpunitConfig->load($phpunitConfigFilename); } else { $phpunitConfig = false; } } if (false !== $phpunitConfig) { - $var = new DOMXpath($phpunitConfig); + $var = new DOMXPath($phpunitConfig); foreach ($var->query('//php/server[@name="'.$name.'"]') as $var) { return $var->getAttribute('value'); } @@ -47,22 +89,36 @@ return $default; }; -if (PHP_VERSION_ID >= 70200) { +$passthruOrFail = function ($command) { + passthru($command, $status); + + if ($status) { + exit($status); + } +}; + +if (\PHP_VERSION_ID >= 80000) { + // PHP 8 requires PHPUnit 9.3+, PHP 8.1 requires PHPUnit 9.5+ + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '9.5') ?: '9.5'; +} elseif (\PHP_VERSION_ID >= 70200) { // PHPUnit 8 requires PHP 7.2+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.2'); -} elseif (PHP_VERSION_ID >= 70100) { + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '8.5') ?: '8.5'; +} elseif (\PHP_VERSION_ID >= 70100) { // PHPUnit 7 requires PHP 7.1+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5'); -} elseif (PHP_VERSION_ID >= 70000) { + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '7.5') ?: '7.5'; +} elseif (\PHP_VERSION_ID >= 70000) { // PHPUnit 6 requires PHP 7.0+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '6.5'); -} elseif (PHP_VERSION_ID >= 50600) { - // PHPUnit 5 requires PHP 5.6+ - $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '5.7'); + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '6.5') ?: '6.5'; +} elseif (\PHP_VERSION_ID >= 50600) { + // PHPUnit 4 does not support PHP 7 + $PHPUNIT_VERSION = $getEnvVar('SYMFONY_PHPUNIT_VERSION', '5.7') ?: '5.7'; } else { + // PHPUnit 5.1 requires PHP 5.6+ $PHPUNIT_VERSION = '4.8'; } +$PHPUNIT_REMOVE_RETURN_TYPEHINT = filter_var($getEnvVar('SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT', '0'), \FILTER_VALIDATE_BOOLEAN); + $COMPOSER_JSON = getenv('COMPOSER') ?: 'composer.json'; $root = __DIR__; @@ -74,18 +130,18 @@ } $oldPwd = getcwd(); -$PHPUNIT_DIR = $getEnvVar('SYMFONY_PHPUNIT_DIR', $root.'/vendor/bin/.phpunit'); -$PHP = defined('PHP_BINARY') ? PHP_BINARY : 'php'; +$PHPUNIT_DIR = rtrim($getEnvVar('SYMFONY_PHPUNIT_DIR', $root.'/vendor/bin/.phpunit'), '/'.\DIRECTORY_SEPARATOR); +$PHP = defined('PHP_BINARY') ? \PHP_BINARY : 'php'; $PHP = escapeshellarg($PHP); -if ('phpdbg' === PHP_SAPI) { +if ('phpdbg' === \PHP_SAPI) { $PHP .= ' -qrr'; } -$defaultEnvs = array( +$defaultEnvs = [ 'COMPOSER' => 'composer.json', 'COMPOSER_VENDOR_DIR' => 'vendor', 'COMPOSER_BIN_DIR' => 'bin', -); +]; foreach ($defaultEnvs as $envName => $envValue) { if ($envValue !== getenv($envName)) { @@ -94,64 +150,159 @@ } } -$COMPOSER = file_exists($COMPOSER = $oldPwd.'/composer.phar') || ($COMPOSER = rtrim('\\' === DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar`) : `which composer.phar 2> /dev/null`)) - ? $PHP.' '.escapeshellarg($COMPOSER) - : 'composer'; - +if ('disabled' === $getEnvVar('SYMFONY_DEPRECATIONS_HELPER')) { + putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); +} -$SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml': '')); +$COMPOSER = ($COMPOSER = getenv('COMPOSER_BINARY')) + || file_exists($COMPOSER = $oldPwd.'/composer.phar') + || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer.phar 2> NUL`) : `which composer.phar 2> /dev/null`))) + || ($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? preg_replace('/[\r\n].*/', '', `where.exe composer 2> NUL`) : `which composer 2> /dev/null`))) + || file_exists($COMPOSER = rtrim((string) ('\\' === \DIRECTORY_SEPARATOR ? `git rev-parse --show-toplevel 2> NUL` : `git rev-parse --show-toplevel 2> /dev/null`)).\DIRECTORY_SEPARATOR.'composer.phar') + ? ('#!/usr/bin/env php' === file_get_contents($COMPOSER, false, null, 0, 18) ? $PHP : '').' '.escapeshellarg($COMPOSER) // detect shell wrappers by looking at the shebang + : 'composer'; -if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__FILE__)."\n".$SYMFONY_PHPUNIT_REMOVE !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION.md5")) { +$prevCacheDir = getenv('COMPOSER_CACHE_DIR'); +if ($prevCacheDir) { + if (false === $absoluteCacheDir = realpath($prevCacheDir)) { + @mkdir($prevCacheDir, 0777, true); + $absoluteCacheDir = realpath($prevCacheDir); + } + if ($absoluteCacheDir) { + putenv("COMPOSER_CACHE_DIR=$absoluteCacheDir"); + } else { + $prevCacheDir = false; + } +} +$SYMFONY_PHPUNIT_REMOVE = $getEnvVar('SYMFONY_PHPUNIT_REMOVE', 'phpspec/prophecy'.($PHPUNIT_VERSION < 6.0 ? ' symfony/yaml' : '')); +$configurationHash = md5(implode(\PHP_EOL, [md5_file(__FILE__), $SYMFONY_PHPUNIT_REMOVE, (int) $PHPUNIT_REMOVE_RETURN_TYPEHINT])); +$PHPUNIT_VERSION_DIR = sprintf('phpunit-%s-%d', $PHPUNIT_VERSION, $PHPUNIT_REMOVE_RETURN_TYPEHINT); +if (!file_exists("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit") || $configurationHash !== @file_get_contents("$PHPUNIT_DIR/.$PHPUNIT_VERSION_DIR.md5")) { // Build a standalone phpunit without symfony/yaml nor prophecy by default @mkdir($PHPUNIT_DIR, 0777, true); chdir($PHPUNIT_DIR); - if (file_exists("phpunit-$PHPUNIT_VERSION")) { - passthru(sprintf('\\' === DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s > NUL': 'rm -rf %s', "phpunit-$PHPUNIT_VERSION.old")); - rename("phpunit-$PHPUNIT_VERSION", "phpunit-$PHPUNIT_VERSION.old"); - passthru(sprintf('\\' === DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s': 'rm -rf %s', "phpunit-$PHPUNIT_VERSION.old")); + if (file_exists("$PHPUNIT_VERSION_DIR")) { + passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s 2> NUL' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old"))); + rename("$PHPUNIT_VERSION_DIR", "$PHPUNIT_VERSION_DIR.old"); + passthru(sprintf('\\' === \DIRECTORY_SEPARATOR ? 'rmdir /S /Q %s' : 'rm -rf %s', escapeshellarg("$PHPUNIT_VERSION_DIR.old"))); + } + + $info = []; + foreach (explode("\n", `$COMPOSER info --no-ansi -a -n phpunit/phpunit "$PHPUNIT_VERSION.*"`) as $line) { + $line = rtrim($line); + + if (!$info && preg_match('/^versions +: /', $line)) { + $info['versions'] = explode(', ', ltrim(substr($line, 9), ': ')); + } elseif (isset($info['requires'])) { + if ('' === $line) { + break; + } + + $line = explode(' ', $line, 2); + $info['requires'][$line[0]] = $line[1]; + } elseif ($info && 'requires' === $line) { + $info['requires'] = []; + } + } + + if (in_array('--colors=never', $argv, true) || (isset($argv[$i = array_search('never', $argv, true) - 1]) && '--colors' === $argv[$i])) { + $COMPOSER .= ' --no-ansi'; + } else { + $COMPOSER .= ' --ansi'; + } + + $info += [ + 'versions' => [], + 'requires' => ['php' => '*'], + ]; + + $stableVersions = array_filter($info['versions'], function ($v) { + return !preg_match('/-dev$|^dev-/', $v); + }); + + if (!$stableVersions) { + $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress -s dev phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); + } else { + $passthruOrFail("$COMPOSER create-project --ignore-platform-reqs --no-install --prefer-dist --no-scripts --no-plugins --no-progress phpunit/phpunit $PHPUNIT_VERSION_DIR \"$PHPUNIT_VERSION.*\""); } - passthru("$COMPOSER create-project --no-install --prefer-dist --no-scripts --no-plugins --no-progress --ansi phpunit/phpunit phpunit-$PHPUNIT_VERSION \"$PHPUNIT_VERSION.*\""); - chdir("phpunit-$PHPUNIT_VERSION"); + + @copy("$PHPUNIT_VERSION_DIR/phpunit.xsd", 'phpunit.xsd'); + chdir("$PHPUNIT_VERSION_DIR"); if ($SYMFONY_PHPUNIT_REMOVE) { - passthru("$COMPOSER remove --no-update ".$SYMFONY_PHPUNIT_REMOVE); + $passthruOrFail("$COMPOSER remove --no-update --no-interaction ".$SYMFONY_PHPUNIT_REMOVE); } if (5.1 <= $PHPUNIT_VERSION && $PHPUNIT_VERSION < 5.4) { - passthru("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); + $passthruOrFail("$COMPOSER require --no-update phpunit/phpunit-mock-objects \"~3.1.0\""); + } + + if (preg_match('{\^((\d++\.)\d++)[\d\.]*$}', $info['requires']['php'], $phpVersion) && version_compare($phpVersion[2].'99', \PHP_VERSION, '<')) { + $passthruOrFail("$COMPOSER config platform.php \"$phpVersion[1].99\""); + } else { + $passthruOrFail("$COMPOSER config --unset platform.php"); } if (file_exists($path = $root.'/vendor/symfony/phpunit-bridge')) { - passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\""); - passthru("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $path))); - if ('\\' === DIRECTORY_SEPARATOR) { + $haystack = "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR"; + $rootLen = strlen($root); + + $p = ($rootLen <= strlen($haystack) ? str_repeat('../', substr_count($haystack, '/', $rootLen)) : '').'vendor/symfony/phpunit-bridge'; + if (realpath($p) === realpath($path)) { + $path = $p; + } + $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*@dev\""); + $passthruOrFail("$COMPOSER config repositories.phpunit-bridge path ".escapeshellarg(str_replace('/', \DIRECTORY_SEPARATOR, $path))); + if ('\\' === \DIRECTORY_SEPARATOR) { file_put_contents('composer.json', preg_replace('/^( {8})"phpunit-bridge": \{$/m', "$0\n$1 ".'"options": {"symlink": false},', file_get_contents('composer.json'))); } } else { - passthru("$COMPOSER require --no-update symfony/phpunit-bridge \"*\""); + $passthruOrFail("$COMPOSER require --no-update symfony/phpunit-bridge \"*\""); } $prevRoot = getenv('COMPOSER_ROOT_VERSION'); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); - $q = '\\' === DIRECTORY_SEPARATOR ? '"' : ''; + $q = '\\' === \DIRECTORY_SEPARATOR && \PHP_VERSION_ID < 80000 ? '"' : ''; // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS - $exit = proc_close(proc_open("$q$COMPOSER install --no-dev --prefer-dist --no-progress --ansi$q", array(), $p, getcwd())); + $exit = proc_close(proc_open("$q$COMPOSER install --no-dev --prefer-dist --no-progress $q", [], $p, getcwd())); putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : '')); + if ($prevCacheDir) { + putenv("COMPOSER_CACHE_DIR=$prevCacheDir"); + } if ($exit) { exit($exit); } + + // Mutate TestCase code + $alteredCode = file_get_contents($alteredFile = './src/Framework/TestCase.php'); + if ($PHPUNIT_REMOVE_RETURN_TYPEHINT) { + $alteredCode = preg_replace('/^ ((?:protected|public)(?: static)? function \w+\(\)): void/m', ' $1', $alteredCode); + } + $alteredCode = preg_replace('/abstract class (?:TestCase|PHPUnit_Framework_TestCase)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillTestCaseTrait;", $alteredCode, 1); + file_put_contents($alteredFile, $alteredCode); + + // Mutate Assert code + $alteredCode = file_get_contents($alteredFile = './src/Framework/Assert.php'); + $alteredCode = preg_replace('/abstract class (?:Assert|PHPUnit_Framework_Assert)[^\{]+\{/', '$0 '.\PHP_EOL." use \Symfony\Bridge\PhpUnit\Legacy\PolyfillAssertTrait;", $alteredCode, 1); + file_put_contents($alteredFile, $alteredCode); + file_put_contents('phpunit', <<<'EOPHP' getExcludedDirectories(); + PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); + class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\ExcludeList::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); +} elseif (method_exists(\PHPUnit\Util\Blacklist::class, 'addDirectory')) { + (new PHPUnit\Util\BlackList())->getBlacklistedDirectories(); + PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListPhpunit::class))->getFileName())); + class_exists(\SymfonyExcludeListSimplePhpunit::class, false) && PHPUnit\Util\Blacklist::addDirectory(\dirname((new \ReflectionClass(\SymfonyExcludeListSimplePhpunit::class))->getFileName())); } else { - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyBlacklistPhpunit'] = 1; - PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyBlacklistSimplePhpunit'] = 1; + PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListPhpunit'] = 1; + PHPUnit\Util\Blacklist::$blacklistedClassNames['SymfonyExcludeListSimplePhpunit'] = 1; } Symfony\Bridge\PhpUnit\TextUI\Command::main(); @@ -159,15 +310,40 @@ class SymfonyBlacklistPhpunit {} EOPHP ); chdir('..'); - file_put_contents(".$PHPUNIT_VERSION.md5", md5_file(__FILE__)."\n".$SYMFONY_PHPUNIT_REMOVE); + file_put_contents(".$PHPUNIT_VERSION_DIR.md5", $configurationHash); chdir($oldPwd); +} + +// Create a symlink with a predictable path pointing to the currently used version. +// This is useful for static analytics tools such as PHPStan having to load PHPUnit's classes +// and for other testing libraries such as Behat using PHPUnit's assertions. +chdir($PHPUNIT_DIR); +if ('\\' === \DIRECTORY_SEPARATOR) { + passthru('rmdir /S /Q phpunit 2> NUL'); + passthru(sprintf('mklink /j phpunit %s > NUL 2>&1', escapeshellarg($PHPUNIT_VERSION_DIR))); +} else { + if (file_exists('phpunit')) { + @unlink('phpunit'); + } + @symlink($PHPUNIT_VERSION_DIR, 'phpunit'); +} +chdir($oldPwd); + +if ($PHPUNIT_VERSION < 8.0) { + $argv = array_filter($argv, function ($v) use (&$argc) { + if ('--do-not-cache-result' !== $v) { + return true; + } + --$argc; + return false; + }); +} elseif (filter_var(getenv('SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE'), \FILTER_VALIDATE_BOOLEAN)) { + $argv[] = '--do-not-cache-result'; + ++$argc; } -global $argv, $argc; -$argv = isset($_SERVER['argv']) ? $_SERVER['argv'] : array(); -$argc = isset($_SERVER['argc']) ? $_SERVER['argc'] : 0; -$components = array(); +$components = []; $cmd = array_map('escapeshellarg', $argv); $exit = 0; @@ -175,7 +351,7 @@ class SymfonyBlacklistPhpunit {} $argv[1] = 'src/Symfony'; } if (isset($argv[1]) && is_dir($argv[1]) && !file_exists($argv[1].'/phpunit.xml.dist')) { - // Find Symfony components in plain PHP for Windows portability + // Find Symfony components in plain php for Windows portability $finder = new RecursiveDirectoryIterator($argv[1], FilesystemIterator::KEY_AS_FILENAME | FilesystemIterator::UNIX_PATHS); $finder = new RecursiveIteratorIterator($finder); @@ -191,10 +367,10 @@ class SymfonyBlacklistPhpunit {} } } -$cmd[0] = sprintf('%s %s --colors=always', $PHP, escapeshellarg("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit")); +$cmd[0] = sprintf('%s %s --colors=always', $PHP, escapeshellarg("$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit")); $cmd = str_replace('%', '%%', implode(' ', $cmd)).' %1$s'; -if ('\\' === DIRECTORY_SEPARATOR) { +if ('\\' === \DIRECTORY_SEPARATOR) { $cmd = 'cmd /v:on /d /c "('.$cmd.')%2$s"'; } else { $cmd .= '%2$s'; @@ -202,7 +378,7 @@ class SymfonyBlacklistPhpunit {} if ($components) { $skippedTests = isset($_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS']) ? $_SERVER['SYMFONY_PHPUNIT_SKIPPED_TESTS'] : false; - $runningProcs = array(); + $runningProcs = []; foreach ($components as $component) { // Run phpunit tests in parallel @@ -213,7 +389,7 @@ class SymfonyBlacklistPhpunit {} $c = escapeshellarg($component); - if ($proc = proc_open(sprintf($cmd, $c, " > $c/phpunit.stdout 2> $c/phpunit.stderr"), array(), $pipes)) { + if ($proc = proc_open(sprintf($cmd, $c, " > $c/phpunit.stdout 2> $c/phpunit.stderr"), [], $pipes)) { $runningProcs[$component] = $proc; } else { $exit = 1; @@ -223,7 +399,7 @@ class SymfonyBlacklistPhpunit {} while ($runningProcs) { usleep(300000); - $terminatedProcs = array(); + $terminatedProcs = []; foreach ($runningProcs as $component => $proc) { $procStatus = proc_get_status($proc); if (!$procStatus['running']) { @@ -234,7 +410,7 @@ class SymfonyBlacklistPhpunit {} } foreach ($terminatedProcs as $component => $procStatus) { - foreach (array('out', 'err') as $file) { + foreach (['out', 'err'] as $file) { $file = "$component/phpunit.std$file"; readfile($file); unlink($file); @@ -244,7 +420,7 @@ class SymfonyBlacklistPhpunit {} // STATUS_STACK_BUFFER_OVERRUN (-1073740791/0xC0000409) // STATUS_ACCESS_VIOLATION (-1073741819/0xC0000005) // STATUS_HEAP_CORRUPTION (-1073740940/0xC0000374) - if ($procStatus && ('\\' !== DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN) || !in_array($procStatus, array(-1073740791, -1073741819, -1073740940)))) { + if ($procStatus && ('\\' !== \DIRECTORY_SEPARATOR || !extension_loaded('apcu') || !filter_var(ini_get('apc.enable_cli'), \FILTER_VALIDATE_BOOLEAN) || !in_array($procStatus, [-1073740791, -1073741819, -1073740940]))) { $exit = $procStatus; echo "\033[41mKO\033[0m $component\n\n"; } else { @@ -253,13 +429,15 @@ class SymfonyBlacklistPhpunit {} } } } elseif (!isset($argv[1]) || 'install' !== $argv[1] || file_exists('install')) { - if (!class_exists('SymfonyBlacklistSimplePhpunit', false)) { - class SymfonyBlacklistSimplePhpunit {} + if (!class_exists(\SymfonyExcludeListSimplePhpunit::class, false)) { + class SymfonyExcludeListSimplePhpunit + { + } } - array_splice($argv, 1, 0, array('--colors=always')); + array_splice($argv, 1, 0, ['--colors=always']); $_SERVER['argv'] = $argv; $_SERVER['argc'] = ++$argc; - include "$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit"; + include "$PHPUNIT_DIR/$PHPUNIT_VERSION_DIR/phpunit"; } exit($exit); diff --git a/src/Symfony/Bridge/PhpUnit/bootstrap.php b/src/Symfony/Bridge/PhpUnit/bootstrap.php index 5de946789155d..d9947a7f4e1c8 100644 --- a/src/Symfony/Bridge/PhpUnit/bootstrap.php +++ b/src/Symfony/Bridge/PhpUnit/bootstrap.php @@ -12,6 +12,96 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler; +if (class_exists(\PHPUnit_Runner_Version::class) && version_compare(\PHPUnit_Runner_Version::id(), '6.0.0', '<')) { + $classes = [ + 'PHPUnit_Framework_Assert', // override PhpUnit's ForwardCompat child class + 'PHPUnit_Framework_AssertionFailedError', // override PhpUnit's ForwardCompat child class + 'PHPUnit_Framework_BaseTestListener', // override PhpUnit's ForwardCompat child class + + 'PHPUnit_Framework_Constraint', + 'PHPUnit_Framework_Constraint_ArrayHasKey', + 'PHPUnit_Framework_Constraint_ArraySubset', + 'PHPUnit_Framework_Constraint_Attribute', + 'PHPUnit_Framework_Constraint_Callback', + 'PHPUnit_Framework_Constraint_ClassHasAttribute', + 'PHPUnit_Framework_Constraint_ClassHasStaticAttribute', + 'PHPUnit_Framework_Constraint_Composite', + 'PHPUnit_Framework_Constraint_Count', + 'PHPUnit_Framework_Constraint_Exception', + 'PHPUnit_Framework_Constraint_ExceptionCode', + 'PHPUnit_Framework_Constraint_ExceptionMessage', + 'PHPUnit_Framework_Constraint_ExceptionMessageRegExp', + 'PHPUnit_Framework_Constraint_FileExists', + 'PHPUnit_Framework_Constraint_GreaterThan', + 'PHPUnit_Framework_Constraint_IsAnything', + 'PHPUnit_Framework_Constraint_IsEmpty', + 'PHPUnit_Framework_Constraint_IsEqual', + 'PHPUnit_Framework_Constraint_IsFalse', + 'PHPUnit_Framework_Constraint_IsIdentical', + 'PHPUnit_Framework_Constraint_IsInstanceOf', + 'PHPUnit_Framework_Constraint_IsJson', + 'PHPUnit_Framework_Constraint_IsNull', + 'PHPUnit_Framework_Constraint_IsTrue', + 'PHPUnit_Framework_Constraint_IsType', + 'PHPUnit_Framework_Constraint_JsonMatches', + 'PHPUnit_Framework_Constraint_JsonMatches_ErrorMessageProvider', + 'PHPUnit_Framework_Constraint_LessThan', + 'PHPUnit_Framework_Constraint_ObjectHasAttribute', + 'PHPUnit_Framework_Constraint_PCREMatch', + 'PHPUnit_Framework_Constraint_SameSize', + 'PHPUnit_Framework_Constraint_StringContains', + 'PHPUnit_Framework_Constraint_StringEndsWith', + 'PHPUnit_Framework_Constraint_StringMatches', + 'PHPUnit_Framework_Constraint_StringStartsWith', + 'PHPUnit_Framework_Constraint_TraversableContains', + 'PHPUnit_Framework_Constraint_TraversableContainsOnly', + + 'PHPUnit_Framework_Error_Deprecated', + 'PHPUnit_Framework_Error_Notice', + 'PHPUnit_Framework_Error_Warning', + 'PHPUnit_Framework_Exception', + 'PHPUnit_Framework_ExpectationFailedException', + + 'PHPUnit_Framework_MockObject_MockObject', + + 'PHPUnit_Framework_IncompleteTest', + 'PHPUnit_Framework_IncompleteTestCase', + 'PHPUnit_Framework_IncompleteTestError', + 'PHPUnit_Framework_RiskyTest', + 'PHPUnit_Framework_RiskyTestError', + 'PHPUnit_Framework_SkippedTest', + 'PHPUnit_Framework_SkippedTestCase', + 'PHPUnit_Framework_SkippedTestError', + 'PHPUnit_Framework_SkippedTestSuiteError', + + 'PHPUnit_Framework_SyntheticError', + + 'PHPUnit_Framework_Test', + 'PHPUnit_Framework_TestCase', // override PhpUnit's ForwardCompat child class + 'PHPUnit_Framework_TestFailure', + 'PHPUnit_Framework_TestListener', + 'PHPUnit_Framework_TestResult', + 'PHPUnit_Framework_TestSuite', // override PhpUnit's ForwardCompat child class + + 'PHPUnit_Runner_BaseTestRunner', + 'PHPUnit_Runner_Version', + + 'PHPUnit_Util_Blacklist', + 'PHPUnit_Util_ErrorHandler', + 'PHPUnit_Util_Test', + 'PHPUnit_Util_XML', + ]; + foreach ($classes as $class) { + class_alias($class, '\\'.strtr($class, '_', '\\')); + } + + class_alias('PHPUnit_Framework_Constraint_And', 'PHPUnit\Framework\Constraint\LogicalAnd'); + class_alias('PHPUnit_Framework_Constraint_Not', 'PHPUnit\Framework\Constraint\LogicalNot'); + class_alias('PHPUnit_Framework_Constraint_Or', 'PHPUnit\Framework\Constraint\LogicalOr'); + class_alias('PHPUnit_Framework_Constraint_Xor', 'PHPUnit\Framework\Constraint\LogicalXor'); + class_alias('PHPUnit_Framework_Error', 'PHPUnit\Framework\Error\Error'); +} + // Detect if we need to serialize deprecations to a file. if ($file = getenv('SYMFONY_DEPRECATIONS_SERIALIZE')) { DeprecationErrorHandler::collectDeprecations($file); @@ -20,15 +110,15 @@ } // Detect if we're loaded by an actual run of phpunit -if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists('PHPUnit_TextUI_Command', false) && !class_exists('PHPUnit\TextUI\Command', false)) { +if (!defined('PHPUNIT_COMPOSER_INSTALL') && !class_exists(\PHPUnit_TextUI_Command::class, false) && !class_exists(\PHPUnit\TextUI\Command::class, false)) { return; } // Enforce a consistent locale -setlocale(LC_ALL, 'C'); +setlocale(\LC_ALL, 'C'); -if (!class_exists('Doctrine\Common\Annotations\AnnotationRegistry', false) && class_exists('Doctrine\Common\Annotations\AnnotationRegistry')) { - if (method_exists('Doctrine\Common\Annotations\AnnotationRegistry', 'registerUniqueLoader')) { +if (!class_exists(AnnotationRegistry::class, false) && class_exists(AnnotationRegistry::class)) { + if (method_exists(AnnotationRegistry::class, 'registerUniqueLoader')) { AnnotationRegistry::registerUniqueLoader('class_exists'); } else { AnnotationRegistry::registerLoader('class_exists'); diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 6d06cb3fefc51..54cb26cf70164 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/phpunit-bridge", "type": "symfony-bridge", - "description": "Symfony PHPUnit Bridge", + "description": "Provides utilities for PHPUnit, especially user deprecation notices management", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -20,11 +20,14 @@ "php": "THIS BRIDGE WHEN TESTING LOWEST SYMFONY VERSIONS.", "php": ">=5.5.9" }, + "require-dev": { + "symfony/error-handler": "^4.4|^5.0" + }, "suggest": { "symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader" }, "conflict": { - "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0" + "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0|<6.4,>=6.0|9.1.2" }, "autoload": { "files": [ "bootstrap.php" ], @@ -38,9 +41,6 @@ ], "minimum-stability": "dev", "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - }, "thanks": { "name": "phpunit/phpunit", "url": "https://github.com/sebastianbergmann/phpunit" diff --git a/src/Symfony/Bridge/ProxyManager/.gitattributes b/src/Symfony/Bridge/ProxyManager/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bridge/ProxyManager/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bridge/ProxyManager/LICENSE b/src/Symfony/Bridge/ProxyManager/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bridge/ProxyManager/LICENSE +++ b/src/Symfony/Bridge/ProxyManager/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php index 7b6ce56b0fbe9..7f5aa8ab51d86 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/Instantiator/RuntimeInstantiator.php @@ -48,7 +48,11 @@ function (&$wrappedInstance, LazyLoadingInterface $proxy) use ($realInstantiator $proxy->setProxyInitializer(null); return true; - } + }, + [ + 'fluentSafe' => $definition->hasTag('proxy'), + 'skipDestructor' => true, + ] ); } } diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php index 545a736711c99 10000 ..d665070f02454 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/LazyLoadingValueHolderGenerator.php @@ -11,90 +11,29 @@ namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper; +use Laminas\Code\Generator\ClassGenerator; use ProxyManager\ProxyGenerator\LazyLoadingValueHolderGenerator as BaseGenerator; use Symfony\Component\DependencyInjection\Definition; -use Zend\Code\Generator\ClassGenerator; /** * @internal */ class LazyLoadingValueHolderGenerator extends BaseGenerator { - private $fluentSafe = false; - - public function setFluentSafe(bool $fluentSafe) - { - $this->fluentSafe = $fluentSafe; - } - /** * {@inheritdoc} */ - public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator) + public function generate(\ReflectionClass $originalClass, ClassGenerator $classGenerator, array $proxyOptions = []): void { - parent::generate($originalClass, $classGenerator); + parent::generate($originalClass, $classGenerator, $proxyOptions); foreach ($classGenerator->getMethods() as $method) { - $body = preg_replace( - '/(\$this->initializer[0-9a-f]++) && \1->__invoke\(\$this->(valueHolder[0-9a-f]++), (.*?), \1\);/', - '$1 && ($1->__invoke(\$$2, $3, $1) || 1) && $this->$2 = \$$2;', - $method->getBody() - ); - $body = str_replace('(new \ReflectionClass(get_class()))', '$reflection', $body); - $body = str_replace('$reflection = $reflection ?: ', '$reflection = $reflection ?? ', $body); - $body = str_replace('$reflection ?? $reflection = ', '$reflection ?? ', $body); - - if ($originalClass->isInterface()) { - $body = str_replace('get_parent_class($this)', var_export($originalClass->name, true), $body); - $body = preg_replace_callback('/\n\n\$realInstanceReflection = [^{]++\{([^}]++)\}\n\n.*/s', function ($m) { - $r = ''; - foreach (explode("\n", $m[1]) as $line) { - $r .= "\n".substr($line, 4); - if (0 === strpos($line, ' return ')) { - break; - } - } - - return $r; - }, $body); - } - - if ($this->fluentSafe) { - $indent = $method->getIndentation(); - $method->setIndentation(''); - $code = $method->generate(); - if (null !== $docBlock = $method->getDocBlock()) { - $code = substr($code, \strlen($docBlock->generate())); - } - $refAmp = (strpos($code, '&') ?: \PHP_INT_MAX) <= strpos($code, '(') ? '&' : ''; - $body = preg_replace( - '/\nreturn (\$this->valueHolder[0-9a-f]++)(->[^;]++);$/', - "\nif ($1 === \$returnValue = {$refAmp}$1$2) {\n \$returnValue = \$this;\n}\n\nreturn \$returnValue;", - $body - ); - $method->setIndentation($indent); + if (str_starts_with($originalClass->getFilename(), __FILE__)) { + $method->setBody(str_replace(var_export($originalClass->name, true), '__CLASS__', $method->getBody())); } - - if (0 === strpos($originalClass->getFilename(), __FILE__)) { - $body = str_replace(var_export($originalClass->name, true), '__CLASS__', $body); - } - - $method->setBody($body); - } - - if ($classGenerator->hasMethod('__destruct')) { - $destructor = $classGenerator->getMethod('__destruct'); - $body = $destructor->getBody(); - $newBody = preg_replace('/^(\$this->initializer[a-zA-Z0-9]++) && .*;\n\nreturn (\$this->valueHolder)/', '$1 || $2', $body); - - if ($body === $newBody) { - throw new \UnexpectedValueException(sprintf('Unexpected lazy-proxy format generated for method %s::__destruct()', $originalClass->name)); - } - - $destructor->setBody($newBody); } - if (0 === strpos($originalClass->getFilename(), __FILE__)) { + if (str_starts_with($originalClass->getFilename(), __FILE__)) { $interfaces = $classGenerator->getImplementedInterfaces(); array_pop($interfaces); $classGenerator->setImplementedInterfaces(array_merge($interfaces, $originalClass->getInterfaceNames())); @@ -104,7 +43,7 @@ public function generate(\ReflectionClass $originalClass, ClassGenerator $classG public function getProxifiedClass(Definition $definition): ?string { if (!$definition->hasTag('proxy')) { - return class_exists($class = $definition->getClass()) || interface_exists($class, false) ? $class : null; + return ($class = $definition->getClass()) && (class_exists($class) || interface_exists($class, false)) ? $class : null; } if (!$definition->isLazy()) { throw new \InvalidArgumentException(sprintf('Invalid definition for service of class "%s": setting the "proxy" tag on a service requires it to be "lazy".', $definition->getClass())); diff --git a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php index e09d54074862d..599fa7c6ec404 100644 --- a/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php +++ b/src/Symfony/Bridge/ProxyManager/LazyProxy/PhpDumper/ProxyDumper.php @@ -11,9 +11,8 @@ namespace Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper; -use ProxyManager\Generator\ClassGenerator; +use Laminas\Code\Generator\ClassGenerator; use ProxyManager\GeneratorStrategy\BaseGeneratorStrategy; -use ProxyManager\Version; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\LazyProxy\PhpDumper\DumperInterface; @@ -40,7 +39,7 @@ public function __construct(string $salt = '') /** * {@inheritdoc} */ - public function isProxyCandidate(Definition $definition) + public function isProxyCandidate(Definition $definition): bool { return ($definition->isLazy() || $definition->hasTag('proxy')) && $this->proxyGenerator->getProxifiedClass($definition); } @@ -48,7 +47,7 @@ public function isProxyCandidate(Definition $definition) /** * {@inheritdoc} */ - public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = null) + public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = null): string { $instantiation = 'return'; @@ -82,30 +81,14 @@ public function getProxyFactoryCode(Definition $definition, $id, $factoryCode = /** * {@inheritdoc} */ - public function getProxyCode(Definition $definition) + public function getProxyCode(Definition $definition): string { $code = $this->classGenerator->generate($this->generateProxyClass($definition)); - - if (version_compare(self::getProxyManagerVersion(), '2.2', '<')) { - $code = preg_replace( - '/((?:\$(?:this|initializer|instance)->)?(?:publicProperties|initializer|valueHolder))[0-9a-f]++/', - '${1}'.$this->getIdentifierSuffix($definition), - $code - ); - } + $code = preg_replace('/^(class [^ ]++ extends )([^\\\\])/', '$1\\\\$2', $code); return $code; } - private static function getProxyManagerVersion(): string - { - if (!class_exists(Version::class)) { - return '0.0.1'; - } - - return \defined(Version::class.'::VERSION') ? Version::VERSION : Version::getVersion(); - } - /** * Produces the proxy class name for the given definition. */ @@ -121,8 +104,10 @@ private function generateProxyClass(Definition $definition): ClassGenerator $generatedClass = new ClassGenerator($this->getProxyClassName($definition)); $class = $this->proxyGenerator->getProxifiedClass($definition); - $this->proxyGenerator->setFluentSafe($definition->hasTag('proxy')); - $this->proxyGenerator->generate(new \ReflectionClass($class), $generatedClass); + $this->proxyGenerator->generate(new \ReflectionClass($class), $generatedClass, [ + 'fluentSafe' => $definition->hasTag('proxy'), + 'skipDestructor' => true, + ]); return $generatedClass; } diff --git a/src/Symfony/Bridge/ProxyManager/README.md b/src/Symfony/Bridge/ProxyManager/README.md index 38d3d6964527f..ff6c6b2f76505 100644 --- a/src/Symfony/Bridge/ProxyManager/README.md +++ b/src/Symfony/Bridge/ProxyManager/README.md @@ -1,14 +1,15 @@ ProxyManager Bridge =================== -Provides integration for [ProxyManager][1] with various Symfony components. +The ProxyManager bridge provides integration for [ProxyManager][1] with various +Symfony components. Resources --------- - * [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) + * [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://github.com/Ocramius/ProxyManager +[1]: https://github.com/FriendsOfPHP/proxy-manager-lts diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php index fc5af78fac182..69b7239655944 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/ContainerBuilderTest.php @@ -14,6 +14,8 @@ require_once __DIR__.'/Fixtures/includes/foo.php'; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; +use ProxyManagerBridgeFooClass; use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -31,7 +33,7 @@ public function testCreateProxyServiceWithRuntimeInstantiator() $builder->setProxyInstantiator(new RuntimeInstantiator()); - $builder->register('foo1', 'ProxyManagerBridgeFooClass')->setFile(__DIR__.'/Fixtures/includes/foo.php')->setPublic(true); + $builder->register('foo1', ProxyManagerBridgeFooClass::class)->setFile(__DIR__.'/Fixtures/includes/foo.php')->setPublic(true); $builder->getDefinition('foo1')->setLazy(true); $builder->compile(); @@ -43,16 +45,16 @@ public function testCreateProxyServiceWithRuntimeInstantiator() $this->assertSame(0, $foo1::$destructorCount); $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved on multiple subsequent calls'); - $this->assertInstanceOf('\ProxyManagerBridgeFooClass', $foo1); - $this->assertInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1); + $this->assertInstanceOf(ProxyManagerBridgeFooClass::class, $foo1); + $this->assertInstanceOf(LazyLoadingInterface::class, $foo1); $this->assertFalse($foo1->isProxyInitialized()); $foo1->initializeProxy(); $this->assertSame($foo1, $builder->get('foo1'), 'The same proxy is retrieved after initialization'); $this->assertTrue($foo1->isProxyInitialized()); - $this->assertInstanceOf('\ProxyManagerBridgeFooClass', $foo1->getWrappedValueHolderValue()); - $this->assertNotInstanceOf('\ProxyManager\Proxy\LazyLoadingInterface', $foo1->getWrappedValueHolderValue()); + $this->assertInstanceOf(ProxyManagerBridgeFooClass::class, $foo1->getWrappedValueHolderValue()); + $this->assertNotInstanceOf(LazyLoadingInterface::class, $foo1->getWrappedValueHolderValue()); $foo1->__destruct(); $this->assertSame(1, $foo1::$destructorCount); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php index 647d1667c3ea6..8bc017bb8df71 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Dumper/PhpDumperTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\ProxyManager\Tests\LazyProxy\Dumper; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; use Symfony\Bridge\ProxyManager\LazyProxy\PhpDumper\ProxyDumper; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Dumper\PhpDumper; @@ -38,15 +39,15 @@ public function testDumpContainerWithProxyService() */ public function testDumpContainerWithProxyServiceWillShareProxies() { - if (!class_exists('LazyServiceProjectServiceContainer', false)) { + if (!class_exists(\LazyServiceProjectServiceContainer::class, false)) { eval('?>'.$this->dumpLazyServiceProjectServiceContainer()); } $container = new \LazyServiceProjectServiceContainer(); $proxy = $container->get('foo'); - $this->assertInstanceOf('stdClass', $proxy); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $proxy); + $this->assertInstanceOf(\stdClass::class, $proxy); + $this->assertInstanceOf(LazyLoadingInterface::class, $proxy); $this->assertSame($proxy, $container->get('foo')); $this->assertFalse($proxy->isProxyInitialized()); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt index 574041b89bbb4..906fff68f7bbe 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Fixtures/php/lazy_service_structure.txt @@ -21,5 +21,5 @@ class LazyServiceProjectServiceContainer extends Container } } -class stdClass_%s extends %SstdClass implements \ProxyManager\%s +class stdClass_%s extends \stdClass implements \ProxyManager\%s {%a}%A diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php index e58b7d6356161..e202fad702655 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/Instantiator/RuntimeInstantiatorTest.php @@ -12,7 +12,10 @@ namespace Symfony\Bridge\ProxyManager\Tests\LazyProxy\Instantiator; use PHPUnit\Framework\TestCase; +use ProxyManager\Proxy\LazyLoadingInterface; +use ProxyManager\Proxy\ValueHolderInterface; use Symfony\Bridge\ProxyManager\LazyProxy\Instantiator\RuntimeInstantiator; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; /** @@ -30,7 +33,7 @@ class RuntimeInstantiatorTest extends TestCase /** * {@inheritdoc} */ - protected function setUp() + protected function setUp(): void { $this->instantiator = new RuntimeInstantiator(); } @@ -38,17 +41,17 @@ protected function setUp() public function testInstantiateProxy() { $instance = new \stdClass(); - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); $definition = new Definition('stdClass'); $instantiator = function () use ($instance) { return $instance; }; - /* @var $proxy \ProxyManager\Proxy\LazyLoadingInterface|\ProxyManager\Proxy\ValueHolderInterface */ + /* @var $proxy LazyLoadingInterface|ValueHolderInterface */ $proxy = $this->instantiator->instantiateProxy($container, $definition, 'foo', $instantiator); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $proxy); - $this->assertInstanceOf('ProxyManager\Proxy\ValueHolderInterface', $proxy); + $this->assertInstanceOf(LazyLoadingInterface::class, $proxy); + $this->assertInstanceOf(ValueHolderInterface::class, $proxy); $this->assertFalse($proxy->isProxyInitialized()); $proxy->initializeProxy(); diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php index 165b0db0cc4aa..19a9bdd5125d3 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/Fixtures/proxy-implem.php @@ -1,22 +1,21 @@ initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'dummy', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, 'dummy', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; - if ($this->valueHolder1eff735 === $returnValue = $this->valueHolder1eff735->dummy()) { - $returnValue = $this; + if ($this->valueHolder%s === $returnValue = $this->valueHolder%s->dummy()) { + return $this; } return $returnValue; @@ -24,10 +23,10 @@ public function dummy() public function & dummyRef() { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'dummyRef', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, 'dummyRef', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; - if ($this->valueHolder1eff735 === $returnValue = &$this->valueHolder1eff735->dummyRef()) { - $returnValue = $this; + if ($this->valueHolder%s === $returnValue = & $this->valueHolder%s->dummyRef()) { + return $this; } return $returnValue; @@ -35,10 +34,10 @@ public function & dummyRef() public function sunny() { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'sunny', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, 'sunny', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; - if ($this->valueHolder1eff735 === $returnValue = $this->valueHolder1eff735->sunny()) { - $returnValue = $this; + if ($this->valueHolder%s === $returnValue = $this->valueHolder%s->sunny()) { + return $this; } return $returnValue; @@ -49,9 +48,9 @@ public static function staticProxyConstructor($initializer) static $reflection; $reflection = $reflection ?? new \ReflectionClass(__CLASS__); - $instance = $reflection->newInstanceWithoutConstructor(); + $instance = $reflection->newInstanceWithoutConstructor(); - $instance->initializer1eff735 = $initializer; + $instance->initializer%s = $initializer; return $instance; } @@ -60,106 +59,169 @@ public function __construct() { static $reflection; - if (! $this->valueHolder1eff735) { + if (! $this->valueHolder%s) { $reflection = $reflection ?? new \ReflectionClass(__CLASS__); - $this->valueHolder1eff735 = $reflection->newInstanceWithoutConstructor(); + $this->valueHolder%s = $reflection->newInstanceWithoutConstructor(); } } public function & __get($name) { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__get', ['name' => $name], $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__get', ['name' => $name], $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; + + if (isset(self::$publicProperties%s[$name])) { + return $this->valueHolder%s->$name; + } - if (isset(self::$publicProperties1eff735[$name])) { - return $this->valueHolder1eff735->$name; + $realInstanceReflection = new \ReflectionClass(__CLASS__); + + if (! $realInstanceReflection->hasProperty($name)) { + $targetObject = $this->valueHolder%s; + + $backtrace = debug_backtrace(false, 1); + trigger_error( + sprintf( + 'Undefined property: %%s::$%%s in %%s on line %%s', + $realInstanceReflection->getName(), + $name, + $backtrace[0]['file'], + $backtrace[0]['line'] + ), + \E_USER_NOTICE + ); + return $targetObject->$name; } - $targetObject = $this->valueHolder1eff735; + $targetObject = $this->valueHolder%s; + $accessor = function & () use ($targetObject, $name) { + return $targetObject->$name; + }; + $backtrace = debug_backtrace(true, 2); + $scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub(); + $accessor = $accessor->bindTo($scopeObject, get_class($scopeObject)); + $returnValue = & $accessor(); - $backtrace = debug_backtrace(false); - trigger_error( - sprintf( - 'Undefined property: %s::$%s in %s on line %s', - __CLASS__, - $name, - $backtrace[0]['file'], - $backtrace[0]['line'] - ), - \E_USER_NOTICE - ); - return $targetObject->$name; + return $returnValue; } public function __set($name, $value) { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__set', array('name' => $name, 'value' => $value), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__set', array('name' => $name, 'value' => $value), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; + + $realInstanceReflection = new \ReflectionClass(__CLASS__); + + if (! $realInstanceReflection->hasProperty($name)) { + $targetObject = $this->valueHolder%s; - $targetObject = $this->valueHolder1eff735; + $targetObject->$name = $value; - return $targetObject->$name = $value; + return $targetObject->$name; + } + + $targetObject = $this->valueHolder%s; + $accessor = function & () use ($targetObject, $name, $value) { + $targetObject->$name = $value; + + return $targetObject->$name; + }; + $backtrace = debug_backtrace(true, 2); + $scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub(); + $accessor = $accessor->bindTo($scopeObject, get_class($scopeObject)); + $returnValue = & $accessor(); + + return $returnValue; } public function __isset($name) { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__isset', array('name' => $name), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__isset', array('name' => $name), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; + + $realInstanceReflection = new \ReflectionClass(__CLASS__); - $targetObject = $this->valueHolder1eff735; + if (! $realInstanceReflection->hasProperty($name)) { + $targetObject = $this->valueHolder%s; - return isset($targetObject->$name); + return isset($targetObject->$name); + } + + $targetObject = $this->valueHolder%s; + $accessor = function () use ($targetObject, $name) { + return isset($targetObject->$name); + }; + $backtrace = debug_backtrace(true, 2); + $scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub(); + $accessor = $accessor->bindTo($scopeObject, get_class($scopeObject)); + $returnValue = $accessor(); + + return $returnValue; } public function __unset($name) { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__unset', array('name' => $name), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__unset', array('name' => $name), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; + + $realInstanceReflection = new \ReflectionClass(__CLASS__); + + if (! $realInstanceReflection->hasProperty($name)) { + $targetObject = $this->valueHolder%s; - $targetObject = $this->valueHolder1eff735; + unset($targetObject->$name); - unset($targetObject->$name); -return; + return; + } + + $targetObject = $this->valueHolder%s; + $accessor = function () use ($targetObject, $name) { + unset($targetObject->$name); + + return; + }; + $backtrace = debug_backtrace(true, 2); + $scopeObject = isset($backtrace[1]['object']) ? $backtrace[1]['object'] : new \ProxyManager\Stub\EmptyClassStub(); + $accessor = $accessor->bindTo($scopeObject, get_class($scopeObject)); + $accessor(); } public function __clone() { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__clone', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__clone', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; - $this->valueHolder1eff735 = clone $this->valueHolder1eff735; + $this->valueHolder%s = clone $this->valueHolder%s; } public function __sleep() { - $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, '__sleep', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, '__sleep', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; - return array('valueHolder1eff735'); + return array('valueHolder%s'); } public function __wakeup() { } - public function setProxyInitializer(\Closure $initializer = null) + public function setProxyInitializer(\Closure $initializer = null)%S { - $this->initializer1eff735 = $initializer; + $this->initializer%s = $initializer; } - public function getProxyInitializer() + public function getProxyInitializer()%S { - return $this->initializer1eff735; + return $this->initializer%s; } public function initializeProxy() : bool { - return $this->initializer1eff735 && ($this->initializer1eff735->__invoke($valueHolder1eff735, $this, 'initializeProxy', array(), $this->initializer1eff735) || 1) && $this->valueHolder1eff735 = $valueHolder1eff735; + return $this->initializer%s && ($this->initializer%s->__invoke($valueHolder%s, $this, 'initializeProxy', array(), $this->initializer%s) || 1) && $this->valueHolder%s = $valueHolder%s; } public function isProxyInitialized() : bool { - return null !== $this->valueHolder1eff735; + return null !== $this->valueHolder%s; } - public function getWrappedValueHolderValue() + public function getWrappedValueHolderValue()%S { - return $this->valueHolder1eff735; - } - - + return $this->valueHolder%s; + }%w } diff --git a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php index 097014c98e6ae..b63a54b7bfbf0 100644 --- a/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php +++ b/src/Symfony/Bridge/ProxyManager/Tests/LazyProxy/PhpDumper/ProxyDumperTest.php @@ -31,7 +31,7 @@ class ProxyDumperTest extends TestCase /** * {@inheritdoc} */ - protected function setUp() + protected function setUp(): void { $this->dumper = new ProxyDumper(); } @@ -109,12 +109,10 @@ public function getPrivatePublicDefinitions() ]; } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Missing factory code to construct the service "foo". - */ public function testGetProxyFactoryCodeWithoutCustomMethod() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Missing factory code to construct the service "foo".'); $definition = new Definition(__CLASS__); $definition->setLazy(true); $this->dumper->getProxyFactoryCode($definition, 'foo'); @@ -155,12 +153,12 @@ protected function createProxy(\$class, \Closure \$factory) EOPHP; $implem = preg_replace('#\n /\*\*.*?\*/#s', '', $implem); - $implem = str_replace('getWrappedValueHolderValue() : ?object', 'getWrappedValueHolderValue()', $implem); $implem = str_replace("array(\n \n );", "[\n \n ];", $implem); - $this->assertStringEqualsFile(__DIR__.'/Fixtures/proxy-implem.php', $implem); + + $this->assertStringMatchesFormatFile(__DIR__.'/Fixtures/proxy-implem.php', $implem); $this->assertStringEqualsFile(__DIR__.'/Fixtures/proxy-factory.php', $factory); - require_once __DIR__.'/Fixtures/proxy-implem.php'; + eval(preg_replace('/^<\?php/', '', $implem)); $factory = require __DIR__.'/Fixtures/proxy-factory.php'; $foo = $factory->getFooService(); @@ -175,10 +173,7 @@ protected function createProxy(\$class, \Closure \$factory) $this->assertSame(123, @$foo->dynamicProp); } - /** - * @return array - */ - public function getProxyCandidates() + public function getProxyCandidates(): array { $definitions = [ [new Definition(__CLASS__), true], @@ -199,8 +194,11 @@ function ($definition) { } } +#[\AllowDynamicProperties] final class DummyClass implements DummyInterface, SunnyInterface { + private $ref; + public function dummy() { return $this; diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index c7038a9c19d48..577138489e690 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/proxy-manager-bridge", "type": "symfony-bridge", - "description": "Symfony ProxyManager Bridge", + "description": "Provides integration for ProxyManager with various Symfony components", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,26 +16,19 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", + "friendsofphp/proxy-manager-lts": "^1.0.2", "symfony/dependency-injection": "^4.0|^5.0", - "ocramius/proxy-manager": "~2.1" + "symfony/polyfill-php80": "^1.16" }, "require-dev": { "symfony/config": "^3.4|^4.0|^5.0" }, - "conflict": { - "zendframework/zend-eventmanager": "2.6.0" - }, "autoload": { "psr-4": { "Symfony\\Bridge\\ProxyManager\\": "" }, "exclude-from-classmap": [ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bridge/Twig/.gitattributes b/src/Symfony/Bridge/Twig/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bridge/Twig/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bridge/Twig/AppVariable.php b/src/Symfony/Bridge/Twig/AppVariable.php index 21020270a06e4..4a22265908b42 100644 --- a/src/Symfony/Bridge/Twig/AppVariable.php +++ b/src/Symfony/Bridge/Twig/AppVariable.php @@ -68,7 +68,7 @@ public function getToken() /** * Returns the current user. * - * @return mixed + * @return object|null * * @see TokenInterface::getUser() */ @@ -79,13 +79,12 @@ public function getUser() } if (!$token = $tokenStorage->getToken()) { - return; + return null; } $user = $token->getUser(); - if (\is_object($user)) { - return $user; - } + + return \is_object($user) ? $user : null; } /** @@ -112,10 +111,9 @@ public function getSession() if (null === $this->requestStack) { throw new \RuntimeException('The "app.session" variable is not available.'); } + $request = $this->getRequest(); - if ($request = $this->getRequest()) { - return $request->getSession(); - } + return $request && $request->hasSession() ? $request->getSession() : null; } /** @@ -157,8 +155,7 @@ public function getDebug() public function getFlashes($types = null) { try { - $session = $this->getSession(); - if (null === $session) { + if (null === $session = $this->getSession()) { return []; } } catch (\RuntimeException $e) { diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index 2fbfe125b5aaf..71a30ae880d78 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -4,8 +4,15 @@ CHANGELOG 4.4.0 ----- + * added a new `TwigErrorRenderer` for `html` format, integrated with the `ErrorHandler` component + * marked all classes extending twig as `@final` * deprecated to pass `$rootDir` and `$fileLinkFormatter` as 5th and 6th argument respectively to the `DebugCommand::__construct()` method, swap the variables position. + * the `LintCommand` lints all the templates stored in all configured Twig paths if none argument is provided + * deprecated accepting STDIN implicitly when using the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. + * added `--show-deprecations` option to the `lint:twig` command + * added support for Bootstrap4 switches: add the `switch-custom` class to the label attributes of a `CheckboxType` + * Marked the `TwigDataCollector` class as `@final`. 4.3.0 ----- diff --git a/src/Symfony/Bridge/Twig/Command/DebugCommand.php b/src/Symfony/Bridge/Twig/Command/DebugCommand.php index e12fd9c04d34f..a6a1e9a2cebde 100644 --- a/src/Symfony/Bridge/Twig/Command/DebugCommand.php +++ b/src/Symfony/Bridge/Twig/Command/DebugCommand.php @@ -56,7 +56,7 @@ public function __construct(Environment $twig, string $projectDir = null, array $this->twigDefaultPath = $twigDefaultPath; if (\is_string($fileLinkFormatter) || $rootDir instanceof FileLinkFormatter) { - @trigger_error(sprintf('Passing a string as "$fileLinkFormatter" 5th argument or an instance of FileLinkFormatter as "$rootDir" 6th argument of the "%s()" method is deprecated since Symfony 4.4, swap the variables position.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing a string as "$fileLinkFormatter" 5th argument or an instance of FileLinkFormatter as "$rootDir" 6th argument of the "%s()" method is deprecated since Symfony 4.4, swap the variables position.', __METHOD__), \E_USER_DEPRECATED); $this->rootDir = $fileLinkFormatter; $this->fileLinkFormatter = $rootDir; @@ -74,7 +74,7 @@ protected function configure() new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'Show details for all entries matching this filter'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (text or json)', 'text'), ]) - ->setDescription('Shows a list of twig functions, filters, globals and tests') + ->setDescription('Show a list of twig functions, filters, globals and tests') ->setHelp(<<<'EOF' The %command.name% command outputs a list of twig functions, filters, globals and tests. @@ -106,17 +106,21 @@ protected function execute(InputInterface $input, OutputInterface $output) $filter = $input->getOption('filter'); if (null !== $name && [] === $this->getFilesystemLoaders()) { - throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s"', FilesystemLoader::class)); + throw new InvalidArgumentException(sprintf('Argument "name" not supported, it requires the Twig loader "%s".', FilesystemLoader::class)); } switch ($input->getOption('format')) { case 'text': - return $name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter); + $name ? $this->displayPathsText($io, $name) : $this->displayGeneralText($io, $filter); + break; case 'json': - return $name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter); + $name ? $this->displayPathsJson($io, $name) : $this->displayGeneralJson($io, $filter); + break; default: throw new InvalidArgumentException(sprintf('The format "%s" is not supported.', $input->getOption('format'))); } + + return 0; } private function displayPathsText(SymfonyStyle $io, string $name) @@ -157,7 +161,7 @@ private function displayPathsText(SymfonyStyle $io, string $name) $shortnames[] = str_replace('\\', '/', $file->getRelativePathname()); } - list($namespace, $shortname) = $this->parseTemplateName($name); + [$namespace, $shortname] = $this->parseTemplateName($name); $alternatives = $this->findAlternatives($shortname, $shortnames); if (FilesystemLoader::MAIN_NAMESPACE !== $namespace) { $alternatives = array_map(function ($shortname) use ($namespace) { @@ -221,7 +225,7 @@ private function displayGeneralText(SymfonyStyle $io, string $filter = null) foreach ($types as $index => $type) { $items = []; foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) { - if (!$filter || false !== strpos($name, $filter)) { + if (!$filter || str_contains($name, $filter)) { $items[$name] = $name.$this->getPrettyMetadata($type, $entity, $decorated); } } @@ -248,14 +252,14 @@ private function displayGeneralText(SymfonyStyle $io, string $filter = null) } } - private function displayGeneralJson(SymfonyStyle $io, $filter) + private function displayGeneralJson(SymfonyStyle $io, ?string $filter) { $decorated = $io->isDecorated(); $types = ['functions', 'filters', 'tests', 'globals']; $data = []; foreach ($types as $type) { foreach ($this->twig->{'get'.ucfirst($type)}() as $name => $entity) { - if (!$filter || false !== strpos($name, $filter)) { + if (!$filter || str_contains($name, $filter)) { $data[$type][$name] = $this->getMetadata($type, $entity); } } @@ -272,7 +276,7 @@ private function displayGeneralJson(SymfonyStyle $io, $filter) $data['warnings'] = $this->buildWarningMessages($wrongBundles); } - $data = json_encode($data, JSON_PRETTY_PRINT); + $data = json_encode($data, \JSON_PRETTY_PRINT); $io->writeln($decorated ? OutputFormatter::escape($data) : $data); } @@ -302,22 +306,22 @@ private function getLoaderPaths(string $name = null): array return $loaderPaths; } - private function getMetadata($type, $entity) + private function getMetadata(string $type, $entity) { if ('globals' === $type) { return $entity; } if ('tests' === $type) { - return; + return null; } if ('functions' === $type || 'filters' === $type) { $cb = $entity->getCallable(); if (null === $cb) { - return; + return null; } if (\is_array($cb)) { if (!method_exists($cb[0], $cb[1])) { - return; + return null; } $refl = new \ReflectionMethod($cb[0], $cb[1]); } elseif (\is_object($cb) && method_exists($cb, '__invoke')) { @@ -327,7 +331,7 @@ private function getMetadata($type, $entity) } elseif (\is_string($cb) && preg_match('{^(.+)::(.+)$}', $cb, $m) && method_exists($m[1], $m[2])) { $refl = new \ReflectionMethod($m[1], $m[2]); } else { - throw new \UnexpectedValueException('Unsupported callback type'); + throw new \UnexpectedValueException('Unsupported callback type.'); } $args = $refl->getParameters(); @@ -356,9 +360,11 @@ private function getMetadata($type, $entity) return $args; } + + return null; } - private function getPrettyMetadata($type, $entity, $decorated) + private function getPrettyMetadata(string $type, $entity, bool $decorated): ?string { if ('tests' === $type) { return ''; @@ -390,6 +396,8 @@ private function getPrettyMetadata($type, $entity, $decorated) if ('filters' === $type) { return $meta ? '('.implode(', ', $meta).')' : ''; } + + return null; } private function findWrongBundleOverrides(): array @@ -398,15 +406,15 @@ private function findWrongBundleOverrides(): array $bundleNames = []; if ($this->rootDir && $this->projectDir) { - $folders = glob($this->rootDir.'/Resources/*/views', GLOB_ONLYDIR); + $folders = glob($this->rootDir.'/Resources/*/views', \GLOB_ONLYDIR); $relativePath = ltrim(substr($this->rootDir.\DIRECTORY_SEPARATOR.'Resources/', \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); $bundleNames = array_reduce($folders, function ($carry, $absolutePath) use ($relativePath) { - if (0 === strpos($absolutePath, $this->projectDir)) { + if (str_starts_with($absolutePath, $this->projectDir)) { $name = basename(\dirname($absolutePath)); $path = ltrim($relativePath.$name, \DIRECTORY_SEPARATOR); $carry[$name] = $path; - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $absolutePath, $this->twigDefaultPath.'/bundles/'.$name), E_USER_DEPRECATED); + @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $absolutePath, $this->twigDefaultPath.'/bundles/'.$name), \E_USER_DEPRECATED); } return $carry; @@ -414,10 +422,10 @@ private function findWrongBundleOverrides(): array } if ($this->twigDefaultPath && $this->projectDir) { - $folders = glob($this->twigDefaultPath.'/bundles/*', GLOB_ONLYDIR); + $folders = glob($this->twigDefaultPath.'/bundles/*', \GLOB_ONLYDIR); $relativePath = ltrim(substr($this->twigDefaultPath.'/bundles/', \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); $bundleNames = array_reduce($folders, function ($carry, $absolutePath) use ($relativePath) { - if (0 === strpos($absolutePath, $this->projectDir)) { + if (str_starts_with($absolutePath, $this->projectDir)) { $name = basename($absolutePath); $path = ltrim($relativePath.$name, \DIRECTORY_SEPARATOR); $carry[$name] = $path; @@ -474,7 +482,7 @@ private function error(SymfonyStyle $io, string $message, array $alternatives = private function findTemplateFiles(string $name): array { - list($namespace, $shortname) = $this->parseTemplateName($name); + [$namespace, $shortname] = $this->parseTemplateName($name); $files = []; foreach ($this->getFilesystemLoaders() as $loader) { @@ -547,21 +555,21 @@ private function findAlternatives(string $name, array $collection): array $alternatives = []; foreach ($collection as $item) { $lev = levenshtein($name, $item); - if ($lev <= \strlen($name) / 3 || false !== strpos($item, $name)) { + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { $alternatives[$item] = isset($alternatives[$item]) ? $alternatives[$item] - $lev : $lev; } } $threshold = 1e3; $alternatives = array_filter($alternatives, function ($lev) use ($threshold) { return $lev < 2 * $threshold; }); - ksort($alternatives, SORT_NATURAL | SORT_FLAG_CASE); + ksort($alternatives, \SORT_NATURAL | \SORT_FLAG_CASE); return array_keys($alternatives); } private function getRelativePath(string $path): string { - if (null !== $this->projectDir && 0 === strpos($path, $this->projectDir)) { + if (null !== $this->projectDir && str_starts_with($path, $this->projectDir)) { return ltrim(substr($path, \strlen($this->projectDir)), \DIRECTORY_SEPARATOR); } @@ -570,7 +578,7 @@ private function getRelativePath(string $path): string private function isAbsolutePath(string $file): bool { - return strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1)) || null !== parse_url($file, PHP_URL_SCHEME); + return strspn($file, '/\\', 0, 1) || (\strlen($file) > 3 && ctype_alpha($file[0]) && ':' === $file[1] && strspn($file, '/\\', 2, 1)) || null !== parse_url($file, \PHP_URL_SCHEME); } /** diff --git a/src/Symfony/Bridge/Twig/Command/LintCommand.php b/src/Symfony/Bridge/Twig/Command/LintCommand.php index d2f7542af7435..9dcbf4964e228 100644 --- a/src/Symfony/Bridge/Twig/Command/LintCommand.php +++ b/src/Symfony/Bridge/Twig/Command/LintCommand.php @@ -23,6 +23,7 @@ use Twig\Environment; use Twig\Error\Error; use Twig\Loader\ArrayLoader; +use Twig\Loader\FilesystemLoader; use Twig\Source; /** @@ -47,16 +48,17 @@ public function __construct(Environment $twig) protected function configure() { $this - ->setDescription('Lints a template and outputs encountered errors') + ->setDescription('Lint a template and outputs encountered errors') ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The output format', 'txt') - ->addArgument('filename', InputArgument::IS_ARRAY) + ->addOption('show-deprecations', null, InputOption::VALUE_NONE, 'Show deprecations as errors') + ->addArgument('filename', InputArgument::IS_ARRAY, 'A file, a directory or "-" for reading from STDIN') ->setHelp(<<<'EOF' The %command.name% command lints a template and outputs to STDOUT the first encountered syntax error. You can validate the syntax of contents passed from STDIN: - cat filename | php %command.full_name% + cat filename | php %command.full_name% - Or the syntax of a file: @@ -76,26 +78,61 @@ protected function execute(InputInterface $input, OutputInterface $output) { $io = new SymfonyStyle($input, $output); $filenames = $input->getArgument('filename'); + $showDeprecations = $input->getOption('show-deprecations'); - if (0 === \count($filenames)) { - if (0 !== ftell(STDIN)) { - throw new RuntimeException('Please provide a filename or pipe template content to STDIN.'); + if (['-'] === $filenames) { + return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); + } + + if (!$filenames) { + // @deprecated to be removed in 5.0 + if (0 === ftell(\STDIN)) { + @trigger_error('Piping content from STDIN to the "lint:twig" command without passing the dash symbol "-" as argument is deprecated since Symfony 4.4.', \E_USER_DEPRECATED); + + return $this->display($input, $output, $io, [$this->validate(file_get_contents('php://stdin'), uniqid('sf_', true))]); } - $template = ''; - while (!feof(STDIN)) { - $template .= fread(STDIN, 1024); + $loader = $this->twig->getLoader(); + if ($loader instanceof FilesystemLoader) { + $paths = []; + foreach ($loader->getNamespaces() as $namespace) { + $paths[] = $loader->getPaths($namespace); + } + $filenames = array_merge(...$paths); } - return $this->display($input, $output, $io, [$this->validate($template, uniqid('sf_', true))]); + if (!$filenames) { + throw new RuntimeException('Please provide a filename or pipe template content to STDIN.'); + } + } + + if ($showDeprecations) { + $prevErrorHandler = set_error_handler(static function ($level, $message, $file, $line) use (&$prevErrorHandler) { + if (\E_USER_DEPRECATED === $level) { + $templateLine = 0; + if (preg_match('/ at line (\d+)[ .]/', $message, $matches)) { + $templateLine = $matches[1]; + } + + throw new Error($message, $templateLine); + } + + return $prevErrorHandler ? $prevErrorHandler($level, $message, $file, $line) : false; + }); } - $filesInfo = $this->getFilesInfo($filenames); + try { + $filesInfo = $this->getFilesInfo($filenames); + } finally { + if ($showDeprecations) { + restore_error_handler(); + } + } return $this->display($input, $output, $io, $filesInfo); } - private function getFilesInfo(array $filenames) + private function getFilesInfo(array $filenames): array { $filesInfo = []; foreach ($filenames as $filename) { @@ -115,16 +152,16 @@ protected function findFiles($filename) return Finder::create()->files()->in($filename)->name('*.twig'); } - throw new RuntimeException(sprintf('File or directory "%s" is not readable', $filename)); + throw new RuntimeException(sprintf('File or directory "%s" is not readable.', $filename)); } - private function validate($template, $file) + private function validate(string $template, string $file): array { $realLoader = $this->twig->getLoader(); try { - $temporaryLoader = new ArrayLoader([(string) $file => $template]); + $temporaryLoader = new ArrayLoader([$file => $template]); $this->twig->setLoader($temporaryLoader); - $nodeTree = $this->twig->parse($this->twig->tokenize(new Source($template, (string) $file))); + $nodeTree = $this->twig->parse($this->twig->tokenize(new Source($template, $file))); $this->twig->compile($nodeTree); $this->twig->setLoader($realLoader); } catch (Error $e) { @@ -136,7 +173,7 @@ private function validate($template, $file) return ['template' => $template, 'file' => $file, 'valid' => true]; } - private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, $files) + private function display(InputInterface $input, OutputInterface $output, SymfonyStyle $io, array $files) { switch ($input->getOption('format')) { case 'txt': @@ -148,7 +185,7 @@ private function display(InputInterface $input, OutputInterface $output, Symfony } } - private function displayTxt(OutputInterface $output, SymfonyStyle $io, $filesInfo) + private function displayTxt(OutputInterface $output, SymfonyStyle $io, array $filesInfo) { $errors = 0; @@ -170,7 +207,7 @@ private function displayTxt(OutputInterface $output, SymfonyStyle $io, $filesInf return min($errors, 1); } - private function displayJson(OutputInterface $output, $filesInfo) + private function displayJson(OutputInterface $output, array $filesInfo) { $errors = 0; @@ -184,12 +221,12 @@ private function displayJson(OutputInterface $output, $filesInfo) } }); - $output->writeln(json_encode($filesInfo, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); + $output->writeln(json_encode($filesInfo, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); return min($errors, 1); } - private function renderException(OutputInterface $output, $template, Error $exception, $file = null) + private function renderException(SymfonyStyle $output, string $template, Error $exception, string $file = null) { $line = $exception->getTemplateLine(); @@ -199,6 +236,14 @@ private function renderException(OutputInterface $output, $template, Error $exce $output->text(sprintf(' ERROR (line %s)', $line)); } + // If the line is not known (this might happen for deprecations if we fail at detecting the line for instance), + // we render the message without context, to ensure the message is displayed. + if ($line <= 0) { + $output->text(sprintf(' >> %s ', $exception->getRawMessage())); + + return; + } + foreach ($this->getContext($template, $line) as $lineNumber => $code) { $output->text(sprintf( '%s %-6s %s', @@ -212,7 +257,7 @@ private function renderException(OutputInterface $output, $template, Error $exce } } - private function getContext($template, $line, $context = 3) + private function getContext(string $template, int $line, int $context = 3) { $lines = explode("\n", $template); diff --git a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php index b7d059daea7c7..c81d16c9606a4 100644 --- a/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php +++ b/src/Symfony/Bridge/Twig/DataCollector/TwigDataCollector.php @@ -25,6 +25,8 @@ * TwigDataCollector. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TwigDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -40,8 +42,10 @@ public function __construct(Profile $profile, Environment $twig = null) /** * {@inheritdoc} + * + * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response, \Exception $exception = null) + public function collect(Request $request, Response $response/* , \Throwable $exception = null */) { } @@ -147,7 +151,7 @@ public function getProfile() return $this->profile; } - private function getComputedData($index) + private function getComputedData(string $index) { if (null === $this->computed) { $this->computed = $this->computeData($this->getProfile()); diff --git a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php new file mode 100644 index 0000000000000..1f5caee4ee5c7 --- /dev/null +++ b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.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\Bridge\Twig\ErrorRenderer; + +use Symfony\Component\ErrorHandler\ErrorRenderer\ErrorRendererInterface; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\HttpFoundation\RequestStack; +use Twig\Environment; +use Twig\Error\LoaderError; +use Twig\Loader\ExistsLoaderInterface; + +/** + * Provides the ability to render custom Twig-based HTML error pages + * in non-debug mode, otherwise falls back to HtmlErrorRenderer. + * + * @author Yonel Ceruto + */ +class TwigErrorRenderer implements ErrorRendererInterface +{ + private $twig; + private $fallbackErrorRenderer; + private $debug; + + /** + * @param bool|callable $debug The debugging mode as a boolean or a callable that should return it + */ + public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, $debug = false) + { + if (!\is_bool($debug) && !\is_callable($debug)) { + throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug))); + } + + $this->twig = $twig; + $this->fallbackErrorRenderer = $fallbackErrorRenderer ?? new HtmlErrorRenderer(); + $this->debug = $debug; + } + + /** + * {@inheritdoc} + */ + public function render(\Throwable $exception): FlattenException + { + $exception = $this->fallbackErrorRenderer->render($exception); + $debug = \is_bool($this->debug) ? $this->debug : ($this->debug)($exception); + + if ($debug || !$template = $this->findTemplate($exception->getStatusCode())) { + return $exception; + } + + return $exception->setAsString($this->twig->render($template, [ + 'legacy' => false, // to be removed in 5.0 + 'exception' => $exception, + 'status_code' => $exception->getStatusCode(), + 'status_text' => $exception->getStatusText(), + ])); + } + + public static function isDebug(RequestStack $requestStack, bool $debug): \Closure + { + return static function () use ($requestStack, $debug): bool { + if (!$request = $requestStack->getCurrentRequest()) { + return $debug; + } + + return $debug && $request->attributes->getBoolean('showException', true); + }; + } + + private function findTemplate(int $statusCode): ?string + { + $template = sprintf('@Twig/Exception/error%s.html.twig', $statusCode); + if ($this->templateExists($template)) { + return $template; + } + + $template = '@Twig/Exception/error.html.twig'; + if ($this->templateExists($template)) { + return $template; + } + + return null; + } + + /** + * To be removed in 5.0. + * + * Use instead: + * + * $this->twig->getLoader()->exists($template) + */ + private function templateExists(string $template): bool + { + $loader = $this->twig->getLoader(); + if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { + return $loader->exists($template); + } + + try { + $loader->getSourceContext($template); + + return true; + } catch (LoaderError $e) { + } + + return false; + } +} diff --git a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php index cc2cdb268e5b5..62e7b91cb2db6 100644 --- a/src/Symfony/Bridge/Twig/Extension/AssetExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/AssetExtension.php @@ -19,6 +19,8 @@ * Twig extension for the Symfony Asset component. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class AssetExtension extends AbstractExtension { @@ -31,6 +33,8 @@ public function __construct(Packages $packages) /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php index 56211fe6ec162..6f50d5a578d07 100644 --- a/src/Symfony/Bridge/Twig/Extension/CodeExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CodeExtension.php @@ -19,6 +19,8 @@ * Twig extension relate to PHP code and used by the profiler and the default exception templates. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class CodeExtension extends AbstractExtension { @@ -33,19 +35,21 @@ class CodeExtension extends AbstractExtension */ public function __construct($fileLinkFormat, string $projectDir, string $charset) { - $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->projectDir = str_replace('\\', '/', $projectDir).'/'; $this->charset = $charset; } /** * {@inheritdoc} + * + * @return TwigFilter[] */ public function getFilters() { return [ - new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html']]), - new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html']]), + new TwigFilter('abbr_class', [$this, 'abbrClass'], ['is_safe' => ['html'], 'pre_escape' => 'html']), + new TwigFilter('abbr_method', [$this, 'abbrMethod'], ['is_safe' => ['html'], 'pre_escape' => 'html']), new TwigFilter('format_args', [$this, 'formatArgs'], ['is_safe' => ['html']]), new TwigFilter('format_args_as_text', [$this, 'formatArgsAsText']), new TwigFilter('file_excerpt', [$this, 'fileExcerpt'], ['is_safe' => ['html']]), @@ -67,8 +71,8 @@ public function abbrClass($class) public function abbrMethod($method) { - if (false !== strpos($method, '::')) { - list($class, $method) = explode('::', $method, 2); + if (str_contains($method, '::')) { + [$class, $method] = explode('::', $method, 2); $result = sprintf('%s::%s()', $this->abbrClass($class), $method); } elseif ('Closure' === $method) { $result = sprintf('%1$s', $method); @@ -91,22 +95,23 @@ public function formatArgs($args) $result = []; foreach ($args as $key => $item) { if ('object' === $item[0]) { + $item[1] = htmlspecialchars($item[1], \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); $parts = explode('\\', $item[1]); $short = array_pop($parts); $formattedValue = sprintf('object(%s)', $item[1], $short); } elseif ('array' === $item[0]) { - $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); + $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)); } elseif ('null' === $item[0]) { $formattedValue = 'null'; } elseif ('boolean' === $item[0]) { - $formattedValue = ''.strtolower(var_export($item[1], true)).''; + $formattedValue = ''.strtolower(htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)).''; } elseif ('resource' === $item[0]) { $formattedValue = 'resource'; } else { - $formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), ENT_COMPAT | ENT_SUBSTITUTE, $this->charset)); + $formattedValue = str_replace("\n", '', htmlspecialchars(var_export($item[1], true), \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset)); } - $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); + $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", htmlspecialchars($key, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $formattedValue); } return implode(', ', $result); @@ -137,7 +142,7 @@ public function fileExcerpt($file, $line, $srcContext = 3) { if (is_file($file) && is_readable($file)) { // highlight_file could throw warnings - // see https://bugs.php.net/bug.php?id=25725 + // see https://bugs.php.net/25725 $code = @highlight_file($file, true); // remove main code/span tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); @@ -174,13 +179,17 @@ public function fileExcerpt($file, $line, $srcContext = 3) public function formatFile($file, $line, $text = null) { $file = trim($file); + $line = (int) $line; if (null === $text) { - $text = $file; - if (null !== $rel = $this->getFileRelative($text)) { - $rel = explode('/', $rel, 2); - $text = sprintf('%s%s', $this->projectDir, $rel[0], '/'.($rel[1] ?? '')); + if (null !== $rel = $this->getFileRelative($file)) { + $rel = explode('/', htmlspecialchars($rel, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), 2); + $text = sprintf('%s%s', htmlspecialchars($this->projectDir, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $rel[0], '/'.($rel[1] ?? '')); + } else { + $text = htmlspecialchars($file, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } + } else { + $text = htmlspecialchars($text, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } if (0 < $line) { @@ -188,7 +197,7 @@ public function formatFile($file, $line, $text = null) } if (false !== $link = $this->getFileLink($file, $line)) { - return sprintf('%s', htmlspecialchars($link, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset), $text); + return sprintf('%s', htmlspecialchars($link, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset), $text); } return $text; @@ -215,7 +224,7 @@ public function getFileRelative(string $file): ?string { $file = str_replace('\\', '/', $file); - if (null !== $this->projectDir && 0 === strpos($file, $this->projectDir)) { + if (null !== $this->projectDir && str_starts_with($file, $this->projectDir)) { return ltrim(substr($file, \strlen($this->projectDir)), '/'); } @@ -232,12 +241,12 @@ public function formatFileFromText($text) /** * @internal */ - public function formatLogMessage($message, array $context) + public function formatLogMessage(string $message, array $context): string { - if ($context && false !== strpos($message, '{')) { + if ($context && str_contains($message, '{')) { $replacements = []; foreach ($context as $key => $val) { - if (is_scalar($val)) { + if (\is_scalar($val)) { $replacements['{'.$key.'}'] = $val; } } @@ -247,7 +256,7 @@ public function formatLogMessage($message, array $context) } } - return htmlspecialchars($message, ENT_COMPAT | ENT_SUBSTITUTE, $this->charset); + return htmlspecialchars($message, \ENT_COMPAT | \ENT_SUBSTITUTE, $this->charset); } /** diff --git a/src/Symfony/Bridge/Twig/Extension/CsrfExtension.php b/src/Symfony/Bridge/Twig/Extension/CsrfExtension.php index 934fe91d7cb5c..79b65c3e6e27d 100644 --- a/src/Symfony/Bridge/Twig/Extension/CsrfExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/CsrfExtension.php @@ -17,6 +17,8 @@ /** * @author Christian Flothmann * @author Titouan Galopin + * + * @final since Symfony 4.4 */ class CsrfExtension extends AbstractExtension { diff --git a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php index 1b2910c830cba..ea857c7ed583b 100644 --- a/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/CsrfRuntime.php @@ -16,6 +16,8 @@ /** * @author Christian Flothmann * @author Titouan Galopin + * + * @final since Symfony 4.4 */ class CsrfRuntime { diff --git a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php index 88b75368da203..8937c890e3a99 100644 --- a/src/Symfony/Bridge/Twig/Extension/DumpExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/DumpExtension.php @@ -17,12 +17,15 @@ use Twig\Environment; use Twig\Extension\AbstractExtension; use Twig\Template; +use Twig\TokenParser\TokenParserInterface; use Twig\TwigFunction; /** * Provides integration of the dump() function with Twig. * * @author Nicolas Grekas + * + * @final since Symfony 4.4 */ class DumpExtension extends AbstractExtension { @@ -35,6 +38,9 @@ public function __construct(ClonerInterface $cloner, HtmlDumper $dumper = null) $this->dumper = $dumper; } + /** + * @return TwigFunction[] + */ public function getFunctions() { return [ @@ -42,6 +48,9 @@ public function getFunctions() ]; } + /** + * @return TokenParserInterface[] + */ public function getTokenParsers() { return [new DumpTokenParser()]; @@ -55,7 +64,7 @@ public function getName() public function dump(Environment $env, $context) { if (!$env->isDebug()) { - return; + return null; } if (2 === \func_num_args()) { @@ -72,8 +81,8 @@ public function dump(Environment $env, $context) unset($vars[0], $vars[1]); } - $dump = fopen('php://memory', 'r+b'); - $this->dumper = $this->dumper ?: new HtmlDumper(); + $dump = fopen('php://memory', 'r+'); + $this->dumper = $this->dumper ?? new HtmlDumper(); $this->dumper->setCharset($env->getCharset()); foreach ($vars as $value) { diff --git a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php index 21f6be4d6ec6d..af7be97c4f9bd 100644 --- a/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ExpressionExtension.php @@ -19,11 +19,15 @@ * ExpressionExtension gives a way to create Expressions from a template. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class ExpressionExtension extends AbstractExtension { /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/FormExtension.php b/src/Symfony/Bridge/Twig/Extension/FormExtension.php index 909e20d58d690..174a5cc3fe4bb 100644 --- a/src/Symfony/Bridge/Twig/Extension/FormExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/FormExtension.php @@ -15,6 +15,7 @@ use Symfony\Component\Form\ChoiceList\View\ChoiceView; use Symfony\Component\Form\FormView; use Twig\Extension\AbstractExtension; +use Twig\TokenParser\TokenParserInterface; use Twig\TwigFilter; use Twig\TwigFunction; use Twig\TwigTest; @@ -24,11 +25,15 @@ * * @author Fabien Potencier * @author Bernhard Schussek + * + * @final since Symfony 4.4 */ class FormExtension extends AbstractExtension { /** * {@inheritdoc} + * + * @return TokenParserInterface[] */ public function getTokenParsers() { @@ -40,6 +45,8 @@ public function getTokenParsers() /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { @@ -60,6 +67,8 @@ public function getFunctions() /** * {@inheritdoc} + * + * @return TwigFilter[] */ public function getFilters() { @@ -71,6 +80,8 @@ public function getFilters() /** * {@inheritdoc} + * + * @return TwigTest[] */ public function getTests() { @@ -100,7 +111,7 @@ public function getName() * * @see ChoiceView::isSelected() */ -function twig_is_selected_choice(ChoiceView $choice, $selectedValue) +function twig_is_selected_choice(ChoiceView $choice, $selectedValue): bool { if (\is_array($selectedValue)) { return \in_array($choice->value, $selectedValue, true); @@ -112,7 +123,7 @@ function twig_is_selected_choice(ChoiceView $choice, $selectedValue) /** * @internal */ -function twig_is_root_form(FormView $formView) +function twig_is_root_form(FormView $formView): bool { return null === $formView->parent; } diff --git a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php index e7421b16d72e3..f3c42482f9d1d 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpFoundationExtension.php @@ -22,6 +22,8 @@ * Twig extension for the Symfony HttpFoundation component. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class HttpFoundationExtension extends AbstractExtension { @@ -42,7 +44,7 @@ public function __construct($urlHelper) throw new \TypeError(sprintf('The first argument must be an instance of "%s" or an instance of "%s".', UrlHelper::class, RequestStack::class)); } - @trigger_error(sprintf('Passing a "%s" instance as the first argument to the "%s" constructor is deprecated since Symfony 4.3, pass a "%s" instance instead.', RequestStack::class, __CLASS__, UrlHelper::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing a "%s" instance as the first argument to the "%s" constructor is deprecated since Symfony 4.3, pass a "%s" instance instead.', RequestStack::class, __CLASS__, UrlHelper::class), \E_USER_DEPRECATED); $requestContext = null; if (2 === \func_num_args()) { @@ -57,6 +59,8 @@ public function __construct($urlHelper) /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php index f8b93ada15475..286bc420c66c5 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelExtension.php @@ -19,9 +19,14 @@ * Provides integration with the HttpKernel component. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class HttpKernelExtension extends AbstractExtension { + /** + * @return TwigFunction[] + */ public function getFunctions() { return [ diff --git a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php index fcd7c24416fbe..edcf8a4dc0e6f 100644 --- a/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php +++ b/src/Symfony/Bridge/Twig/Extension/HttpKernelRuntime.php @@ -18,6 +18,8 @@ * Provides integration with the HttpKernel component. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class HttpKernelRuntime { @@ -40,7 +42,7 @@ public function __construct(FragmentHandler $handler) */ public function renderFragment($uri, $options = []) { - $strategy = isset($options['strategy']) ? $options['strategy'] : 'inline'; + $strategy = $options['strategy'] ?? 'inline'; unset($options['strategy']); return $this->handler->render($uri, $strategy, $options); diff --git a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php index e8bc6190cd65a..a6648dc072db1 100644 --- a/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/LogoutUrlExtension.php @@ -19,6 +19,8 @@ * LogoutUrlHelper provides generator functions for the logout URL to Twig. * * @author Jeremy Mikola + * + * @final since Symfony 4.4 */ class LogoutUrlExtension extends AbstractExtension { @@ -31,6 +33,8 @@ public function __construct(LogoutUrlGenerator $generator) /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php index 21214f81196ad..a46f2cdbb8936 100644 --- a/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/ProfilerExtension.php @@ -17,6 +17,8 @@ /** * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class ProfilerExtension extends BaseProfilerExtension { @@ -31,6 +33,9 @@ public function __construct(Profile $profile, Stopwatch $stopwatch = null) $this->events = new \SplObjectStorage(); } + /** + * @return void + */ public function enter(Profile $profile) { if ($this->stopwatch && $profile->isTemplate()) { @@ -40,6 +45,9 @@ public function enter(Profile $profile) parent::enter($profile); } + /** + * @return void + */ public function leave(Profile $profile) { parent::leave($profile); diff --git a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php index 67fbe8d3910a3..1ba528546d6d2 100644 --- a/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/RoutingExtension.php @@ -22,6 +22,8 @@ * Provides integration of the Routing component with Twig. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class RoutingExtension extends AbstractExtension { @@ -33,9 +35,9 @@ public function __construct(UrlGeneratorInterface $generator) } /** - * Returns a list of functions to add to the existing list. + * {@inheritdoc} * - * @return array An array of functions + * @return TwigFunction[] */ public function getFunctions() { @@ -93,7 +95,7 @@ public function getUrl($name, $parameters = [], $schemeRelative = false) * * @final */ - public function isUrlGenerationSafe(Node $argsNode) + public function isUrlGenerationSafe(Node $argsNode): array { // support named arguments $paramsNode = $argsNode->hasNode('parameters') ? $argsNode->getNode('parameters') : ( diff --git a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php index 439c31aad3df2..4acd7bbf9cc72 100644 --- a/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/SecurityExtension.php @@ -21,6 +21,8 @@ * SecurityExtension exposes security context features. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class SecurityExtension extends AbstractExtension { @@ -50,6 +52,8 @@ public function isGranted($role, $object = null, $field = null) /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php index 19dfed23e3bcd..f4b9a24ced5cd 100644 --- a/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/StopwatchExtension.php @@ -14,11 +14,14 @@ use Symfony\Bridge\Twig\TokenParser\StopwatchTokenParser; use Symfony\Component\Stopwatch\Stopwatch; use Twig\Extension\AbstractExtension; +use Twig\TokenParser\TokenParserInterface; /** * Twig extension for the stopwatch helper. * * @author Wouter J + * + * @final since Symfony 4.4 */ class StopwatchExtension extends AbstractExtension { @@ -36,6 +39,9 @@ public function getStopwatch() return $this->stopwatch; } + /** + * @return TokenParserInterface[] + */ public function getTokenParsers() { return [ diff --git a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php index 9e927ecdf0320..96df106307da7 100644 --- a/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/TranslationExtension.php @@ -24,6 +24,10 @@ use Twig\TokenParser\AbstractTokenParser; use Twig\TwigFilter; +// Help opcache.preload discover always-needed symbols +class_exists(TranslatorInterface::class); +class_exists(TranslatorTrait::class); + /** * Provides integration of the Translation component with Twig. * @@ -42,7 +46,7 @@ class TranslationExtension extends AbstractExtension public function __construct($translator = null, NodeVisitorInterface $translationNodeVisitor = null) { if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); } $this->translator = $translator; $this->translationNodeVisitor = $translationNodeVisitor; @@ -68,6 +72,8 @@ public function getTranslator() /** * {@inheritdoc} + * + * @return TwigFilter[] */ public function getFilters() { @@ -100,6 +106,8 @@ public function getTokenParsers() /** * {@inheritdoc} + * + * @return NodeVisitorInterface[] */ public function getNodeVisitors() { diff --git a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php index 0ca519ee72423..c2c6f8ba8fcf6 100644 --- a/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WebLinkExtension.php @@ -11,9 +11,9 @@ namespace Symfony\Bridge\Twig\Extension; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\GenericLinkProvider; +use Symfony\Component\WebLink\Link; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; @@ -21,6 +21,8 @@ * Twig extension for the Symfony WebLink component. * * @author KΓ©vin Dunglas + * + * @final since Symfony 4.4 */ class WebLinkExtension extends AbstractExtension { @@ -33,6 +35,8 @@ public function __construct(RequestStack $requestStack) /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { diff --git a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php index 85b4f7a4d73cd..b5f3badea88fd 100644 --- a/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/WorkflowExtension.php @@ -21,6 +21,8 @@ * WorkflowExtension. * * @author GrΓ©goire Pineau + * + * @final since Symfony 4.4 */ class WorkflowExtension extends AbstractExtension { @@ -31,6 +33,9 @@ public function __construct(Registry $workflowRegistry) $this->workflowRegistry = $workflowRegistry; } + /** + * @return TwigFunction[] + */ public function getFunctions() { return [ @@ -112,7 +117,7 @@ public function getMarkedPlaces($subject, $placesNameOnly = true, $name = null) * Use a string (the place name) to get place metadata * Use a Transition instance to get transition metadata */ - public function getMetadata($subject, string $key, $metadataSubject = null, string $name = null): ?string + public function getMetadata($subject, string $key, $metadataSubject = null, string $name = null) { return $this ->workflowRegistry diff --git a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php index 3284ec5cd3d06..02d19d8ae3d3b 100644 --- a/src/Symfony/Bridge/Twig/Extension/YamlExtension.php +++ b/src/Symfony/Bridge/Twig/Extension/YamlExtension.php @@ -20,11 +20,15 @@ * Provides integration of the Yaml component with Twig. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class YamlExtension extends AbstractExtension { /** * {@inheritdoc} + * + * @return TwigFilter[] */ public function getFilters() { diff --git a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php index 8dc8998a747d5..1e97ce3371d1d 100644 --- a/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php +++ b/src/Symfony/Bridge/Twig/Form/TwigRendererEngine.php @@ -154,7 +154,7 @@ protected function loadResourcesFromTheme($cacheKey, &$theme) { if (!$theme instanceof Template) { /* @var Template $theme */ - $theme = $this->environment->loadTemplate($theme); + $theme = $this->environment->load($theme)->unwrap(); } if (null === $this->template) { diff --git a/src/Symfony/Bridge/Twig/LICENSE b/src/Symfony/Bridge/Twig/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bridge/Twig/LICENSE +++ b/src/Symfony/Bridge/Twig/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index 2f1a1fb049e7f..166b3c195ff17 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -13,6 +13,7 @@ use League\HTMLToMarkdown\HtmlConverter; use Symfony\Component\Mime\BodyRendererInterface; +use Symfony\Component\Mime\Exception\InvalidArgumentException; use Symfony\Component\Mime\Message; use Twig\Environment; @@ -44,7 +45,20 @@ public function render(Message $message): void return; } - $vars = array_merge($this->context, $message->getContext(), [ + $messageContext = $message->getContext(); + + $previousRenderingKey = $messageContext[__CLASS__] ?? null; + unset($messageContext[__CLASS__]); + $currentRenderingKey = $this->getFingerPrint($message); + if ($previousRenderingKey === $currentRenderingKey) { + return; + } + + if (isset($messageContext['email'])) { + throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', \get_class($message))); + } + + $vars = array_merge($this->context, $messageContext, [ 'email' => new WrappedTemplatedEmail($this->twig, $message), ]); @@ -60,6 +74,24 @@ public function render(Message $message): void if (!$message->getTextBody() && null !== $html = $message->getHtmlBody()) { $message->text($this->convertHtmlToText(\is_resource($html) ? stream_get_contents($html) : $html)); } + $message->context($message->getContext() + [__CLASS__ => $currentRenderingKey]); + } + + private function getFingerPrint(TemplatedEmail $message): string + { + $messageContext = $message->getContext(); + unset($messageContext[__CLASS__]); + + $payload = [$messageContext, $message->getTextTemplate(), $message->getHtmlTemplate()]; + try { + $serialized = serialize($payload); + } catch (\Exception $e) { + // Serialization of 'Closure' is not allowed + // Happens when context contain a closure, in that case, we assume that context always change. + $serialized = random_bytes(8); + } + + return md5($serialized); } private function convertHtmlToText(string $html): string @@ -68,6 +100,6 @@ private function convertHtmlToText(string $html): string return $this->converter->convert($html); } - return strip_tags($html); + return strip_tags(preg_replace('{<(head|style)\b.*?}is', '', $html)); } } diff --git a/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php new file mode 100644 index 0000000000000..382928a982da2 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Mime/NotificationEmail.php @@ -0,0 +1,230 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Mime; + +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\Mime\Header\Headers; +use Symfony\Component\Mime\Part\AbstractPart; +use Twig\Extra\CssInliner\CssInlinerExtension; +use Twig\Extra\Inky\InkyExtension; +use Twig\Extra\Markdown\MarkdownExtension; + +/** + * @author Fabien Potencier + */ +class NotificationEmail extends TemplatedEmail +{ + public const IMPORTANCE_URGENT = 'urgent'; + public const IMPORTANCE_HIGH = 'high'; + public const IMPORTANCE_MEDIUM = 'medium'; + public const IMPORTANCE_LOW = 'low'; + + private $theme = 'default'; + private $context = [ + 'importance' => self::IMPORTANCE_LOW, + 'content' => '', + 'exception' => false, + 'action_text' => null, + 'action_url' => null, + 'markdown' => false, + 'raw' => false, + ]; + + public function __construct(Headers $headers = null, AbstractPart $body = null) + { + $missingPackages = []; + if (!class_exists(CssInlinerExtension::class)) { + $missingPackages['twig/cssinliner-extra'] = 'CSS Inliner'; + } + + if (!class_exists(InkyExtension::class)) { + $missingPackages['twig/inky-extra'] = 'Inky'; + } + + if ($missingPackages) { + throw new \LogicException(sprintf('You cannot use "%s" if the "%s" Twig extension%s not available; try running "%s".', static::class, implode('" and "', $missingPackages), \count($missingPackages) > 1 ? 's are' : ' is', 'composer require '.implode(' ', array_keys($missingPackages)))); + } + + parent::__construct($headers, $body); + } + + /** + * @return $this + */ + public function markdown(string $content) + { + if (!class_exists(MarkdownExtension::class)) { + throw new \LogicException(sprintf('You cannot use "%s" if the Markdown Twig extension is not available; try running "composer require twig/markdown-extra".', __METHOD__)); + } + + $this->context['markdown'] = true; + + return $this->content($content); + } + + /** + * @return $this + */ + public function content(string $content, bool $raw = false) + { + $this->context['content'] = $content; + $this->context['raw'] = $raw; + + return $this; + } + + /** + * @return $this + */ + public function action(string $text, string $url) + { + $this->context['action_text'] = $text; + $this->context['action_url'] = $url; + + return $this; + } + + /** + * @return $this + */ + public function importance(string $importance) + { + $this->context['importance'] = $importance; + + return $this; + } + + /** + * @param \Throwable|FlattenException $exception + * + * @return $this + */ + public function exception($exception) + { + if (!$exception instanceof \Throwable && !$exception instanceof FlattenException) { + throw new \LogicException(sprintf('"%s" accepts "%s" or "%s" instances.', __METHOD__, \Throwable::class, FlattenException::class)); + } + + $exceptionAsString = $this->getExceptionAsString($exception); + + $this->context['exception'] = true; + $this->attach($exceptionAsString, 'exception.txt', 'text/plain'); + $this->importance(self::IMPORTANCE_URGENT); + + if (!$this->getSubject()) { + $this->subject($exception->getMessage()); + } + + return $this; + } + + /** + * @return $this + */ + public function theme(string $theme) + { + $this->theme = $theme; + + return $this; + } + + public function getTextTemplate(): ?string + { + if ($template = parent::getTextTemplate()) { + return $template; + } + + return '@email/'.$this->theme.'/notification/body.txt.twig'; + } + + public function getHtmlTemplate(): ?string + { + if ($template = parent::getHtmlTemplate()) { + return $template; + } + + return '@email/'.$this->theme.'/notification/body.html.twig'; + } + + public function getContext(): array + { + return array_merge($this->context, parent::getContext()); + } + + public function getPreparedHeaders(): Headers + { + $headers = parent::getPreparedHeaders(); + + $importance = $this->context['importance'] ?? self::IMPORTANCE_LOW; + $this->priority($this->determinePriority($importance)); + $headers->setHeaderBody('Text', 'Subject', sprintf('[%s] %s', strtoupper($importance), $this->getSubject())); + + return $headers; + } + + private function determinePriority(string $importance): int + { + switch ($importance) { + case self::IMPORTANCE_URGENT: + return self::PRIORITY_HIGHEST; + case self::IMPORTANCE_HIGH: + return self::PRIORITY_HIGH; + case self::IMPORTANCE_MEDIUM: + return self::PRIORITY_NORMAL; + case self::IMPORTANCE_LOW: + default: + return self::PRIORITY_LOW; + } + } + + private function getExceptionAsString($exception): string + { + if (class_exists(FlattenException::class)) { + $exception = $exception instanceof FlattenException ? $exception : FlattenException::createFromThrowable($exception); + + return $exception->getAsString(); + } + + $message = \get_class($exception); + if ('' !== $exception->getMessage()) { + $message .= ': '.$exception->getMessage(); + } + + $message .= ' in '.$exception->getFile().':'.$exception->getLine()."\n"; + $message .= "Stack trace:\n".$exception->getTraceAsString()."\n\n"; + + return rtrim($message); + } + + /** + * @internal + */ + public function __serialize(): array + { + return [$this->context, $this->theme, parent::__serialize()]; + } + + /** + * @internal + */ + public function __unserialize(array $data): void + { + if (3 === \count($data)) { + [$this->context, $this->theme, $parentData] = $data; + } else { + // Backwards compatibility for deserializing data structures that were serialized without the theme + [$this->context, $parentData] = $data; + } + + parent::__unserialize($parentData); + } +} diff --git a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php index afde5f933d30a..f1726914b490b 100644 --- a/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php +++ b/src/Symfony/Bridge/Twig/Mime/WrappedTemplatedEmail.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Twig\Mime; use Symfony\Component\Mime\Address; -use Symfony\Component\Mime\NamedAddress; use Twig\Environment; /** @@ -33,9 +32,7 @@ public function __construct(Environment $twig, TemplatedEmail $message) public function toName(): string { - $to = $this->message->getTo()[0]; - - return $to instanceof NamedAddress ? $to->getName() : ''; + return $this->message->getTo()[0]->getName(); } public function image(string $image, string $contentType = null): string @@ -63,7 +60,7 @@ public function attach(string $file, string $name = null, string $contentType = /** * @return $this */ - public function setSubject(string $subject) + public function setSubject(string $subject): self { $this->message->subject($subject); @@ -78,7 +75,7 @@ public function getSubject(): ?string /** * @return $this */ - public function setReturnPath(string $address) + public function setReturnPath(string $address): self { $this->message->returnPath($address); @@ -93,15 +90,15 @@ public function getReturnPath(): string /** * @return $this */ - public function addFrom(string $address, string $name = null) + public function addFrom(string $address, string $name = ''): self { - $this->message->addFrom($name ? new NamedAddress($address, $name) : new Address($address)); + $this->message->addFrom(new Address($address, $name)); return $this; } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getFrom(): array { @@ -111,7 +108,7 @@ public function getFrom(): array /** * @return $this */ - public function addReplyTo(string $address) + public function addReplyTo(string $address): self { $this->message->addReplyTo($address); @@ -129,15 +126,15 @@ public function getReplyTo(): array /** * @return $this */ - public function addTo(string $address, string $name = null) + public function addTo(string $address, string $name = ''): self { - $this->message->addTo($name ? new NamedAddress($address, $name) : new Address($address)); + $this->message->addTo(new Address($address, $name)); return $this; } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getTo(): array { @@ -147,15 +144,15 @@ public function getTo(): array /** * @return $this */ - public function addCc(string $address, string $name = null) + public function addCc(string $address, string $name = ''): self { - $this->message->addCc($name ? new NamedAddress($address, $name) : new Address($address)); + $this->message->addCc(new Address($address, $name)); return $this; } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getCc(): array { @@ -165,15 +162,15 @@ public function getCc(): array /** * @return $this */ - public function addBcc(string $address, string $name = null) + public function addBcc(string $address, string $name = ''): self { - $this->message->addBcc($name ? new NamedAddress($address, $name) : new Address($address)); + $this->message->addBcc(new Address($address, $name)); return $this; } /** - * @return (Address|NamedAddress)[] + * @return Address[] */ public function getBcc(): array { @@ -183,9 +180,9 @@ public function getBcc(): array /** * @return $this */ - public function setPriority(int $priority) + public function setPriority(int $priority): self { - $this->message->setPriority($priority); + $this->message->priority($priority); return $this; } diff --git a/src/Symfony/Bridge/Twig/Node/DumpNode.php b/src/Symfony/Bridge/Twig/Node/DumpNode.php index c9cf1e1689ee6..d82d9ade1feaf 100644 --- a/src/Symfony/Bridge/Twig/Node/DumpNode.php +++ b/src/Symfony/Bridge/Twig/Node/DumpNode.php @@ -16,6 +16,8 @@ /** * @author Julien Galenski + * + * @final since Symfony 4.4 * 741A / class DumpNode extends Node { @@ -33,7 +35,7 @@ public function __construct($varPrefix, Node $values = null, int $lineno, string } /** - * {@inheritdoc} + * @return void */ public function compile(Compiler $compiler) { diff --git a/src/Symfony/Bridge/Twig/Node/FormThemeNode.php b/src/Symfony/Bridge/Twig/Node/FormThemeNode.php index c99675cab3168..b17243060f302 100644 --- a/src/Symfony/Bridge/Twig/Node/FormThemeNode.php +++ b/src/Symfony/Bridge/Twig/Node/FormThemeNode.php @@ -17,6 +17,8 @@ /** * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class FormThemeNode extends Node { @@ -25,6 +27,9 @@ public function __construct(Node $form, Node $resources, int $lineno, string $ta parent::__construct(['form' => $form, 'resources' => $resources], ['only' => $only], $lineno, $tag); } + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler diff --git a/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php index dc7d860793f48..29402a8024fae 100644 --- a/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php +++ b/src/Symfony/Bridge/Twig/Node/RenderBlockNode.php @@ -21,9 +21,14 @@ * is "foo", the block "foo" will be rendered. * * @author Bernhard Schussek + * + * @final since Symfony 4.4 */ class RenderBlockNode extends FunctionExpression { + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler->addDebugInfo($this); diff --git a/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php b/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php index 612bec14e5329..bf22c329d6a13 100644 --- a/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php +++ b/src/Symfony/Bridge/Twig/Node/SearchAndRenderBlockNode.php @@ -18,9 +18,14 @@ /** * @author Bernhard Schussek + * + * @final since Symfony 4.4 */ class SearchAndRenderBlockNode extends FunctionExpression { + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler->addDebugInfo($this); @@ -28,7 +33,6 @@ public function compile(Compiler $compiler) preg_match('/_([^_]+)$/', $this->getAttribute('name'), $matches); - $label = null; $arguments = iterator_to_array($this->getNode('arguments')); $blockNameSuffix = $matches[1]; @@ -41,7 +45,7 @@ public function compile(Compiler $compiler) // The "label" function expects the label in the second and // the variables in the third argument $label = $arguments[1]; - $variables = isset($arguments[2]) ? $arguments[2] : null; + $variables = $arguments[2] ?? null; $lineno = $label->getTemplateLine(); if ($label instanceof ConstantExpression) { diff --git a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php index 3844b2124c38f..b4dd8a9b37b4f 100644 --- a/src/Symfony/Bridge/Twig/Node/StopwatchNode.php +++ b/src/Symfony/Bridge/Twig/Node/StopwatchNode.php @@ -19,6 +19,8 @@ * Represents a stopwatch node. * * @author Wouter J + * + * @final since Symfony 4.4 */ class StopwatchNode extends Node { @@ -27,6 +29,9 @@ public function __construct(Node $name, Node $body, AssignNameExpression $var, i parent::__construct(['body' => $body, 'name' => $name, 'var' => $var], [], $lineno, $tag); } + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler diff --git a/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php b/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php index 7f8024aa85640..49ceac1404c5e 100644 --- a/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransDefaultDomainNode.php @@ -17,6 +17,8 @@ /** * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TransDefaultDomainNode extends Node { @@ -25,6 +27,9 @@ public function __construct(AbstractExpression $expr, int $lineno = 0, string $t parent::__construct(['expr' => $expr], [], $lineno, $tag); } + /** + * @return void + */ public function compile(Compiler $compiler) { // noop as this node is just a marker for TranslationDefaultDomainNodeVisitor diff --git a/src/Symfony/Bridge/Twig/Node/TransNode.php b/src/Symfony/Bridge/Twig/Node/TransNode.php index cedc6b740e08d..bc87d75bd7db2 100644 --- a/src/Symfony/Bridge/Twig/Node/TransNode.php +++ b/src/Symfony/Bridge/Twig/Node/TransNode.php @@ -20,10 +20,12 @@ use Twig\Node\TextNode; // BC/FC with namespaced Twig -class_exists('Twig\Node\Expression\ArrayExpression'); +class_exists(ArrayExpression::class); /** * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TransNode extends Node { @@ -46,6 +48,9 @@ public function __construct(Node $body, Node $domain = null, AbstractExpression parent::__construct($nodes, [], $lineno, $tag); } + /** + * @return void + */ public function compile(Compiler $compiler) { $compiler->addDebugInfo($this); @@ -55,9 +60,7 @@ public function compile(Compiler $compiler) $defaults = $this->getNode('vars'); $vars = null; } - list($msg, $defaults) = $this->compileString($this->getNode('body'), $defaults, (bool) $vars); - - $method = !$this->hasNode('count') ? 'trans' : 'transChoice'; + [$msg, $defaults] = $this->compileString($this->getNode('body'), $defaults, (bool) $vars); $compiler ->write('echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans(') diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php index 04b68ef6be199..72badea2d2bd0 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationDefaultDomainNodeVisitor.php @@ -27,6 +27,8 @@ /** * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TranslationDefaultDomainNodeVisitor extends AbstractNodeVisitor { @@ -39,6 +41,8 @@ public function __construct() /** * {@inheritdoc} + * + * @return Node */ protected function doEnterNode(Node $node, Environment $env) { @@ -91,6 +95,8 @@ protected function doEnterNode(Node $node, Environment $env) /** * {@inheritdoc} + * + * @return Node|null */ protected function doLeaveNode(Node $node, Environment $env) { @@ -107,16 +113,15 @@ protected function doLeaveNode(Node $node, Environment $env) /** * {@inheritdoc} + * + * @return int */ public function getPriority() { return -10; } - /** - * @return bool - */ - private function isNamedArguments($arguments) + private function isNamedArguments(Node $arguments): bool { foreach ($arguments as $name => $node) { if (!\is_int($name)) { @@ -127,7 +132,7 @@ private function isNamedArguments($arguments) return false; } - private function getVarName() + private function getVarName(): string { return sprintf('__internal_%s', hash('sha256', uniqid(mt_rand(), true), false)); } diff --git a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php index 3da4141cdd2e0..b9b5959bbd766 100644 --- a/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php +++ b/src/Symfony/Bridge/Twig/NodeVisitor/TranslationNodeVisitor.php @@ -22,20 +22,28 @@ * TranslationNodeVisitor extracts translation messages. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TranslationNodeVisitor extends AbstractNodeVisitor { - const UNDEFINED_DOMAIN = '_undefined'; + public const UNDEFINED_DOMAIN = '_undefined'; private $enabled = false; private $messages = []; + /** + * @return void + */ public function enable() { $this->enabled = true; $this->messages = []; } + /** + * @return void + */ public function disable() { $this->enabled = false; @@ -49,6 +57,8 @@ public function getMessages() /** * {@inheritdoc} + * + * @return Node */ protected function doEnterNode(Node $node, Environment $env) { @@ -89,6 +99,8 @@ protected function doEnterNode(Node $node, Environment $env) /** * {@inheritdoc} + * + * @return Node|null */ protected function doLeaveNode(Node $node, Environment $env) { @@ -97,6 +109,8 @@ protected function doLeaveNode(Node $node, Environment $env) /** * {@inheritdoc} + * + * @return int */ public function getPriority() { diff --git a/src/Symfony/Bridge/Twig/README.md b/src/Symfony/Bridge/Twig/README.md index 602f5a54c3dd6..533d573dbcabe 100644 --- a/src/Symfony/Bridge/Twig/README.md +++ b/src/Symfony/Bridge/Twig/README.md @@ -1,13 +1,13 @@ Twig Bridge =========== -Provides integration for [Twig](https://twig.symfony.com/) with various -Symfony components. +The Twig bridge provides integration for [Twig](https://twig.symfony.com/) with +various Symfony components. Resources --------- - * [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) + * [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/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.html.twig new file mode 100644 index 0000000000000..9027546861a14 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.html.twig @@ -0,0 +1 @@ +{% extends "@email/zurb_2/notification/body.html.twig" %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.txt.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.txt.twig new file mode 100644 index 0000000000000..37671b1f28455 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/default/notification/body.txt.twig @@ -0,0 +1 @@ +{% extends "@email/zurb_2/notification/body.txt.twig" %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/main.css b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/main.css new file mode 100644 index 0000000000000..b826813ec5d76 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/main.css @@ -0,0 +1,1667 @@ +/* + * Copyright (c) 2017 ZURB, inc. -- MIT License + * + * https://raw.githubusercontent.com/foundation/foundation-emails/v2.2.1/dist/foundation-emails.css + */ + +.wrapper { + width: 100%; +} + +#outlook a { + padding: 0; +} + +body { + width: 100% !important; + min-width: 100%; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + margin: 0; + Margin: 0; + padding: 0; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; +} + +.ExternalClass { + width: 100%; +} + +.ExternalClass, +.ExternalClass p, +.ExternalClass span, +.ExternalClass font, +.ExternalClass td, +.ExternalClass div { + line-height: 100%; +} + +#backgroundTable { + margin: 0; + Margin: 0; + padding: 0; + width: 100% !important; + line-height: 100% !important; +} + +img { + outline: none; + text-decoration: none; + -ms-interpolation-mode: bicubic; + width: auto; + max-width: 100%; + clear: both; + display: block; +} + +center { + width: 100%; + min-width: 580px; +} + +a img { + border: none; +} + +p { + margin: 0 0 0 10px; + Margin: 0 0 0 10px; +} + +table { + border-spacing: 0; + border-collapse: collapse; +} + +td { + word-wrap: break-word; + -webkit-hyphens: auto; + -moz-hyphens: auto; + hyphens: auto; + border-collapse: collapse !important; +} + +table, +tr, +td { + padding: 0; + vertical-align: top; + text-align: left; +} + +@media only screen { + html { + min-height: 100%; + background: #f3f3f3; + } +} + +table.body { + background: #f3f3f3; + height: 100%; + width: 100%; +} + +table.container { + background: #fefefe; + width: 580px; + margin: 0 auto; + Margin: 0 auto; + text-align: inherit; +} + +table.row { + padding: 0; + width: 100%; + position: relative; +} + +table.spacer { + width: 100%; +} + +table.spacer td { + mso-line-height-rule: exactly; +} + +table.container table.row { + display: table; +} + +td.columns, +td.column, +th.columns, +th.column { + margin: 0 auto; + Margin: 0 auto; + padding-left: 16px; + padding-bottom: 16px; +} + +td.columns .column, +td.columns .columns, +td.column .column, +td.column .columns, +th.columns .column, +th.columns .columns, +th.column .column, +th.column .columns { + padding-left: 0 !important; + padding-right: 0 !important; +} + +td.columns .column center, +td.columns .columns center, +td.column .column center, +td.column .columns center, +th.columns .column center, +th.columns .columns center, +th.column .column center, +th.column .columns center { + min-width: none !important; +} + +td.columns.last, +td.column.last, +th.columns.last, +th.column.last { + padding-right: 16px; +} + +td.columns table:not(.button), +td.column table:not(.button), +th.columns table:not(.button), +th.column table:not(.button) { + width: 100%; +} + +td.large-1, +th.large-1 { + width: 32.33333px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-1.first, +th.large-1.first { + padding-left: 16px; +} + +td.large-1.last, +th.large-1.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-1, +.collapse>tbody>tr>th.large-1 { + padding-right: 0; + padding-left: 0; + width: 48.33333px; +} + +.collapse td.large-1.first, +.collapse th.large-1.first, +.collapse td.large-1.last, +.collapse th.large-1.last { + width: 56.33333px; +} + +td.large-1 center, +th.large-1 center { + min-width: 0.33333px; +} + +.body .columns td.large-1, +.body .column td.large-1, +.body .columns th.large-1, +.body .column th.large-1 { + width: 8.33333%; +} + +td.large-2, +th.large-2 { + width: 80.66667px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-2.first, +th.large-2.first { + padding-left: 16px; +} + +td.large-2.last, +th.large-2.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-2, +.collapse>tbody>tr>th.large-2 { + padding-right: 0; + padding-left: 0; + width: 96.66667px; +} + +.collapse td.large-2.first, +.collapse th.large-2.first, +.collapse td.large-2.last, +.collapse th.large-2.last { + width: 104.66667px; +} + +td.large-2 center, +th.large-2 center { + min-width: 48.66667px; +} + +.body .columns td.large-2, +.body .column td.large-2, +.body .columns th.large-2, +.body .column th.large-2 { + width: 16.66667%; +} + +td.large-3, +th.large-3 { + width: 129px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-3.first, +th.large-3.first { + padding-left: 16px; +} + +td.large-3.last, +th.large-3.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-3, +.collapse>tbody>tr>th.large-3 { + padding-right: 0; + padding-left: 0; + width: 145px; +} + +.collapse td.large-3.first, +.collapse th.large-3.first, +.collapse td.large-3.last, +.collapse th.large-3.last { + width: 153px; +} + +td.large-3 center, +th.large-3 center { + min-width: 97px; +} + +.body .columns td.large-3, +.body .column td.large-3, +.body .columns th.large-3, +.body .column th.large-3 { + width: 25%; +} + +td.large-4, +th.large-4 { + width: 177.33333px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-4.first, +th.large-4.first { + padding-left: 16px; +} + +td.large-4.last, +th.large-4.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-4, +.collapse>tbody>tr>th.large-4 { + padding-right: 0; + padding-left: 0; + width: 193.33333px; +} + +.collapse td.large-4.first, +.collapse th.large-4.first, +.collapse td.large-4.last, +.collapse th.large-4.last { + width: 201.33333px; +} + +td.large-4 center, +th.large-4 center { + min-width: 145.33333px; +} + +.body .columns td.large-4, +.body .column td.large-4, +.body .columns th.large-4, +.body .column th.large-4 { + width: 33.33333%; +} + +td.large-5, +th.large-5 { + width: 225.66667px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-5.first, +th.large-5.first { + padding-left: 16px; +} + +td.large-5.last, +th.large-5.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-5, +.collapse>tbody>tr>th.large-5 { + padding-right: 0; + padding-left: 0; + width: 241.66667px; +} + +.collapse td.large-5.first, +.collapse th.large-5.first, +.collapse td.large-5.last, +.collapse th.large-5.last { + width: 249.66667px; +} + +td.large-5 center, +th.large-5 center { + min-width: 193.66667px; +} + +.body .columns td.large-5, +.body .column td.large-5, +.body .columns th.large-5, +.body .column th.large-5 { + width: 41.66667%; +} + +td.large-6, +th.large-6 { + width: 274px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-6.first, +th.large-6.first { + padding-left: 16px; +} + +td.large-6.last, +th.large-6.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-6, +.collapse>tbody>tr>th.large-6 { + padding-right: 0; + padding-left: 0; + width: 290px; +} + +.collapse td.large-6.first, +.collapse th.large-6.first, +.collapse td.large-6.last, +.collapse th.large-6.last { + width: 298px; +} + +td.large-6 center, +th.large-6 center { + min-width: 242px; +} + +.body .columns td.large-6, +.body .column td.large-6, +.body .columns th.large-6, +.body .column th.large-6 { + width: 50%; +} + +td.large-7, +th.large-7 { + width: 322.33333px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-7.first, +th.large-7.first { + padding-left: 16px; +} + +td.large-7.last, +th.large-7.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-7, +.collapse>tbody>tr>th.large-7 { + padding-right: 0; + padding-left: 0; + width: 338.33333px; +} + +.collapse td.large-7.first, +.collapse th.large-7.first, +.collapse td.large-7.last, +.collapse th.large-7.last { + width: 346.33333px; +} + +td.large-7 center, +th.large-7 center { + min-width: 290.33333px; +} + +.body .columns td.large-7, +.body .column td.large-7, +.body .columns th.large-7, +.body .column th.large-7 { + width: 58.33333%; +} + +td.large-8, +th.large-8 { + width: 370.66667px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-8.first, +th.large-8.first { + padding-left: 16px; +} + +td.large-8.last, +th.large-8.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-8, +.collapse>tbody>tr>th.large-8 { + padding-right: 0; + padding-left: 0; + width: 386.66667px; +} + +.collapse td.large-8.first, +.collapse th.large-8.first, +.collapse td.large-8.last, +.collapse th.large-8.last { + width: 394.66667px; +} + +td.large-8 center, +th.large-8 center { + min-width: 338.66667px; +} + +.body .columns td.large-8, +.body .column td.large-8, +.body .columns th.large-8, +.body .column th.large-8 { + width: 66.66667%; +} + +td.large-9, +th.large-9 { + width: 419px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-9.first, +th.large-9.first { + padding-left: 16px; +} + +td.large-9.last, +th.large-9.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-9, +.collapse>tbody>tr>th.large-9 { + padding-right: 0; + padding-left: 0; + width: 435px; +} + +.collapse td.large-9.first, +.collapse th.large-9.first, +.collapse td.large-9.last, +.collapse th.large-9.last { + width: 443px; +} + +td.large-9 center, +th.large-9 center { + min-width: 387px; +} + +.body .columns td.large-9, +.body .column td.large-9, +.body .columns th.large-9, +.body .column th.large-9 { + width: 75%; +} + +td.large-10, +th.large-10 { + width: 467.33333px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-10.first, +th.large-10.first { + padding-left: 16px; +} + +td.large-10.last, +th.large-10.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-10, +.collapse>tbody>tr>th.large-10 { + padding-right: 0; + padding-left: 0; + width: 483.33333px; +} + +.collapse td.large-10.first, +.collapse th.large-10.first, +.collapse td.large-10.last, +.collapse th.large-10.last { + width: 491.33333px; +} + +td.large-10 center, +th.large-10 center { + min-width: 435.33333px; +} + +.body .columns td.large-10, +.body .column td.large-10, +.body .columns th.large-10, +.body .column th.large-10 { + width: 83.33333%; +} + +td.large-11, +th.large-11 { + width: 515.66667px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-11.first, +th.large-11.first { + padding-left: 16px; +} + +td.large-11.last, +th.large-11.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-11, +.collapse>tbody>tr>th.large-11 { + padding-right: 0; + padding-left: 0; + width: 531.66667px; +} + +.collapse td.large-11.first, +.collapse th.large-11.first, +.collapse td.large-11.last, +.collapse th.large-11.last { + width: 539.66667px; +} + +td.large-11 center, +th.large-11 center { + min-width: 483.66667px; +} + +.body .columns td.large-11, +.body .column td.large-11, +.body .columns th.large-11, +.body .column th.large-11 { + width: 91.66667%; +} + +td.large-12, +th.large-12 { + width: 564px; + padding-left: 8px; + padding-right: 8px; +} + +td.large-12.first, +th.large-12.first { + padding-left: 16px; +} + +td.large-12.last, +th.large-12.last { + padding-right: 16px; +} + +.collapse>tbody>tr>td.large-12, +.collapse>tbody>tr>th.large-12 { + padding-right: 0; + padding-left: 0; + width: 580px; +} + +.collapse td.large-12.first, +.collapse th.large-12.first, +.collapse td.large-12.last, +.collapse th.large-12.last { + width: 588px; +} + +td.large-12 center, +th.large-12 center { + min-width: 532px; +} + +.body .columns td.large-12, +.body .column td.large-12, +.body .columns th.large-12, +.body .column th.large-12 { + width: 100%; +} + +td.large-offset-1, +td.large-offset-1.first, +td.large-offset-1.last, +th.large-offset-1, +th.large-offset-1.first, +th.large-offset-1.last { + padding-left: 64.33333px; +} + +td.large-offset-2, +td.large-offset-2.first, +td.large-offset-2.last, +th.large-offset-2, +th.large-offset-2.first, +th.large-offset-2.last { + padding-left: 112.66667px; +} + +td.large-offset-3, +td.large-offset-3.first, +td.large-offset-3.last, +th.large-offset-3, +th.large-offset-3.first, +th.large-offset-3.last { + padding-left: 161px; +} + +td.large-offset-4, +td.large-offset-4.first, +td.large-offset-4.last, +th.large-offset-4, +th.large-offset-4.first, +th.large-offset-4.last { + padding-left: 209.33333px; +} + +td.large-offset-5, +td.large-offset-5.first, +td.large-offset-5.last, +th.large-offset-5, +th.large-offset-5.first, +th.large-offset-5.last { + padding-left: 257.66667px; +} + +td.large-offset-6, +td.large-offset-6.first, +td.large-offset-6.last, +th.large-offset-6, +th.large-offset-6.first, +th.large-offset-6.last { + padding-left: 306px; +} + +td.large-offset-7, +td.large-offset-7.first, +td.large-offset-7.last, +th.large-offset-7, +th.large-offset-7.first, +th.large-offset-7.last { + padding-left: 354.33333px; +} + +td.large-offset-8, +td.large-offset-8.first, +td.large-offset-8.last, +th.large-offset-8, +th.large-offset-8.first, +th.large-offset-8.last { + padding-left: 402.66667px; +} + +td.large-offset-9, +td.large-offset-9.first, +td.large-offset-9.last, +th.large-offset-9, +th.large-offset-9.first, +th.large-offset-9.last { + padding-left: 451px; +} + +td.large-offset-10, +td.large-offset-10.first, +td.large-offset-10.last, +th.large-offset-10, +th.large-offset-10.first, +th.large-offset-10.last { + padding-left: 499.33333px; +} + +td.large-offset-11, +td.large-offset-11.first, +td.large-offset-11.last, +th.large-offset-11, +th.large-offset-11.first, +th.large-offset-11.last { + padding-left: 547.66667px; +} + +td.expander, +th.expander { + visibility: hidden; + width: 0; + padding: 0 !important; +} + +table.container.radius { + border-radius: 0; + border-collapse: separate; +} + +.block-grid { + width: 100%; + max-width: 580px; +} + +.block-grid td { + display: inline-block; + padding: 8px; +} + +.up-2 td { + width: 274px !important; +} + +.up-3 td { + width: 177px !important; +} + +.up-4 td { + width: 129px !important; +} + +.up-5 td { + width: 100px !important; +} + +.up-6 td { + width: 80px !important; +} + +.up-7 td { + width: 66px !important; +} + +.up-8 td { + width: 56px !important; +} + +table.text-center, +th.text-center, +td.text-center, +h1.text-center, +h2.text-center, +h3.text-center, +h4.text-center, +h5.text-center, +h6.text-center, +p.text-center, +span.text-center { + text-align: center; +} + +table.text-left, +th.text-left, +td.text-left, +h1.text-left, +h2.text-left, +h3.text-left, +h4.text-left, +h5.text-left, +h6.text-left, +p.text-left, +span.text-left { + text-align: left; +} + +table.text-right, +th.text-right, +td.text-right, +h1.text-right, +h2.text-right, +h3.text-right, +h4.text-right, +h5.text-right, +h6.text-right, +p.text-right, +span.text-right { + text-align: right; +} + +span.text-center { + display: block; + width: 100%; + text-align: center; +} + +@media only screen and (max-width: 596px) { + .small-float-center { + margin: 0 auto !important; + float: none !important; + text-align: center !important; + } + .small-text-center { + text-align: center !important; + } + .small-text-left { + text-align: left !important; + } + .small-text-right { + text-align: right !important; + } +} + +img.float-left { + float: left; + text-align: left; +} + +img.float-right { + float: right; + text-align: right; +} + +img.float-center, +img.text-center { + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; +} + +table.float-center, +td.float-center, +th.float-center { + margin: 0 auto; + Margin: 0 auto; + float: none; + text-align: center; +} + +.hide-for-large { + display: none !important; + mso-hide: all; + overflow: hidden; + max-height: 0; + font-size: 0; + width: 0; + line-height: 0; +} + +@media only screen and (max-width: 596px) { + .hide-for-large { + display: block !important; + width: auto !important; + overflow: visible !important; + max-height: none !important; + font-size: inherit !important; + line-height: inherit !important; + } +} + +table.body table.container .hide-for-large * { + mso-hide: all; +} + +@media only screen and (max-width: 596px) { + table.body table.container .hide-for-large, + table.body table.container .row.hide-for-large { + display: table !important; + width: 100% !important; + } +} + +@media only screen and (max-width: 596px) { + table.body table.container .callout-inner.hide-for-large { + display: table-cell !important; + width: 100% !important; + } +} + +@media only screen and (max-width: 596px) { + table.body table.container .show-for-large { + display: none !important; + width: 0; + mso-hide: all; + overflow: hidden; + } +} + +body, +table.body, +h1, +h2, +h3, +h4, +h5, +h6, +p, +td, +th, +a { + color: #0a0a0a; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + padding: 0; + margin: 0; + Margin: 0; + text-align: left; + line-height: 1.3; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: inherit; + word-wrap: normal; + font-family: Helvetica, Arial, sans-serif; + font-weight: normal; + margin-bottom: 10px; + Margin-bottom: 10px; +} + +h1 { + font-size: 34px; +} + +h2 { + font-size: 30px; +} + +h3 { + font-size: 28px; +} + +h4 { + font-size: 24px; +} + +h5 { + font-size: 20px; +} + +h6 { + font-size: 18px; +} + +body, +table.body, +p, +td, +th { + font-size: 16px; + line-height: 1.3; +} + +p { + margin-bottom: 10px; + Margin-bottom: 10px; +} + +p.lead { + font-size: 20px; + line-height: 1.6; +} + +p.subheader { + margin-top: 4px; + margin-bottom: 8px; + Margin-top: 4px; + Margin-bottom: 8px; + font-weight: normal; + line-height: 1.4; + color: #8a8a8a; +} + +small { + font-size: 80%; + color: #cacaca; +} + +a { + color: #2199e8; + t F438 ext-decoration: none; +} + +a:hover { + color: #147dc2; +} + +a:active { + color: #147dc2; +} + +a:visited { + color: #2199e8; +} + +h1 a, +h1 a:visited, +h2 a, +h2 a:visited, +h3 a, +h3 a:visited, +h4 a, +h4 a:visited, +h5 a, +h5 a:visited, +h6 a, +h6 a:visited { + color: #2199e8; +} + +pre { + background: #f3f3f3; + margin: 30px 0; + Margin: 30px 0; +} + +pre code { + color: #cacaca; +} + +pre code span.callout { + color: #8a8a8a; + font-weight: bold; +} + +pre code span.callout-strong { + color: #ff6908; + font-weight: bold; +} + +table.hr { + width: 100%; +} + +table.hr th { + height: 0; + max-width: 580px; + border-top: 0; + border-right: 0; + border-bottom: 1px solid #0a0a0a; + border-left: 0; + margin: 20px auto; + Margin: 20px auto; + clear: both; +} + +.stat { + font-size: 40px; + line-height: 1; +} + +p+.stat { + margin-top: -16px; + Margin-top: -16px; +} + +span.preheader { + display: none !important; + visibility: hidden; + mso-hide: all !important; + font-size: 1px; + color: #f3f3f3; + line-height: 1px; + max-height: 0px; + max-width: 0px; + opacity: 0; + overflow: hidden; +} + +table.button { + width: auto; + margin: 0 0 16px 0; + Margin: 0 0 16px 0; +} + +table.button table td { + text-align: left; + color: #fefefe; + background: #2199e8; + border: 2px solid #2199e8; +} + +table.button table td a { + font-family: Helvetica, Arial, sans-serif; + font-size: 16px; + font-weight: bold; + color: #fefefe; + text-decoration: none; + display: inline-block; + padding: 8px 16px 8px 16px; + border: 0 solid #2199e8; + border-radius: 3px; +} + +table.button.radius table td { + border-radius: 3px; + border: none; +} + +table.button.rounded table td { + border-radius: 500px; + border: none; +} + +table.button:hover table tr td a, +table.button:active table tr td a, +table.button table tr td a:visited, +table.button.tiny:hover table tr td a, +table.button.tiny:active table tr td a, +table.button.tiny table tr td a:visited, +table.button.small:hover table tr td a, +table.button.small:active table tr td a, +table.button.small table tr td a:visited, +table.button.large:hover table tr td a, +table.button.large:active table tr td a, +table.button.large table tr td a:visited { + color: #fefefe; +} + +table.button.tiny table td, +table.button.tiny table a { + padding: 4px 8px 4px 8px; +} + +table.button.tiny table a { + font-size: 10px; + font-weight: normal; +} + +table.button.small table td, +table.button.small table a { + padding: 5px 10px 5px 10px; + font-size: 12px; +} + +table.button.large table a { + padding: 10px 20px 10px 20px; + font-size: 20px; +} + +table.button.expand, +table.button.expanded { + width: 100% !important; +} + +table.button.expand table, +table.button.expanded table { + width: 100%; +} + +table.button.expand table a, +table.button.expanded table a { + text-align: center; + width: 100%; + padding-left: 0; + padding-right: 0; +} + +table.button.expand center, +table.button.expanded center { + min-width: 0; +} + +table.button:hover table td, +table.button:visited table td, +table.button:active table td { + background: #147dc2; + color: #fefefe; +} + +table.button:hover table a, +table.button:visited table a, +table.button:active table a { + border: 0 solid #147dc2; +} + +table.button.secondary table td { + background: #777777; + color: #fefefe; + border: 0px solid #777777; +} + +table.button.secondary table a { + color: #fefefe; + border: 0 solid #777777; +} + +table.button.secondary:hover table td { + background: #919191; + color: #fefefe; +} + +table.button.secondary:hover table a { + border: 0 solid #919191; +} + +table.button.secondary:hover table td a { + color: #fefefe; +} + +table.button.secondary:active table td a { + color: #fefefe; +} + +table.button.secondary table td a:visited { + color: #fefefe; +} + +table.button.success table td { + background: #3adb76; + border: 0px solid #3adb76; +} + +table.button.success table a { + border: 0 solid #3adb76; +} + +table.button.success:hover table td { + background: #23bf5d; +} + +table.button.success:hover table a { + border: 0 solid #23bf5d; +} + +table.button.alert table td { + background: #ec5840; + border: 0px solid #ec5840; +} + +table.button.alert table a { + border: 0 solid #ec5840; +} + +table.button.alert:hover table td { + background: #e23317; +} + +table.button.alert:hover table a { + border: 0 solid #e23317; +} + +table.button.warning table td { + background: #ffae00; + border: 0px solid #ffae00; +} + +table.button.warning table a { + border: 0px solid #ffae00; +} + +table.button.warning:hover table td { + background: #cc8b00; +} + +table.button.warning:hover table a { + border: 0px solid #cc8b00; +} + +table.callout { + margin-bottom: 16px; + Margin-bottom: 16px; +} + +th.callout-inner { + width: 100%; + border: 1px solid #cbcbcb; + padding: 10px; + background: #fefefe; +} + +th.callout-inner.primary { + background: #def0fc; + border: 1px solid #444444; + color: #0a0a0a; +} + +th.callout-inner.secondary { + background: #ebebeb; + border: 1px solid #444444; + color: #0a0a0a; +} + +th.callout-inner.success { + background: #e1faea; + border: 1px solid #1b9448; + color: #fefefe; +} + +th.callout-inner.warning { + background: #fff3d9; + border: 1px solid #996800; + color: #fefefe; +} + +th.callout-inner.alert { + background: #fce6e2; + border: 1px solid #b42912; + color: #fefefe; +} + +.thumbnail { + border: solid 4px #fefefe; + box-shadow: 0 0 0 1px rgba(10, 10, 10, 0.2); + display: inline-block; + line-height: 0; + max-width: 100%; + transition: box-shadow 200ms ease-out; + border-radius: 3px; + margin-bottom: 16px; +} + +.thumbnail:hover, +.thumbnail:focus { + box-shadow: 0 0 6px 1px rgba(33, 153, 232, 0.5); +} + +table.menu { + width: 580px; +} + +table.menu td.menu-item, +table.menu th.menu-item { + padding: 10px; + padding-right: 10px; +} + +table.menu td.menu-item a, +table.menu th.menu-item a { + color: #2199e8; +} + +table.menu.vertical td.menu-item, +table.menu.vertical th.menu-item { + padding: 10px; + padding-right: 0; + display: block; +} + +table.menu.vertical td.menu-item a, +table.menu.vertical th.menu-item a { + width: 100%; +} + +table.menu.vertical td.menu-item table.menu.vertical td.menu-item, +table.menu.vertical td.menu-item table.menu.vertical th.menu-item, +table.menu.vertical th.menu-item table.menu.vertical td.menu-item, +table.menu.vertical th.menu-item table.menu.vertical th.menu-item { + padding-left: 10px; +} + +table.menu.text-center a { + text-align: center; +} + +.menu[align="center"] { + width: auto !important; +} + +body.outlook p { + display: inline !important; +} + +@media only screen and (max-width: 596px) { + table.body img { + width: auto; + height: auto; + } + table.body center { + min-width: 0 !important; + } + table.body .container { + width: 95% !important; + } + table.body .columns, + table.body .column { + height: auto !important; + -moz-box-sizing: border-box; + -webkit-box-sizing: border-box; + box-sizing: border-box; + padding-left: 16px !important; + padding-right: 16px !important; + } + table.body .columns .column, + table.body .columns .columns, + table.body .column .column, + table.body .column .columns { + padding-left: 0 !important; + padding-right: 0 !important; + } + table.body .collapse .columns, + table.body .collapse .column { + padding-left: 0 !important; + padding-right: 0 !important; + } + td.small-1, + th.small-1 { + display: inline-block !important; + width: 8.33333% !important; + } + td.small-2, + th.small-2 { + display: inline-block !important; + width: 16.66667% !important; + } + td.small-3, + th.small-3 { + display: inline-block !important; + width: 25% !important; + } + td.small-4, + th.small-4 { + display: inline-block !important; + width: 33.33333% !important; + } + td.small-5, + th.small-5 { + display: inline-block !important; + width: 41.66667% !important; + } + td.small-6, + th.small-6 { + display: inline-block !important; + width: 50% !important; + } + td.small-7, + th.small-7 { + display: inline-block !important; + width: 58.33333% !important; + } + td.small-8, + th.small-8 { + display: inline-block !important; + width: 66.66667% !important; + } + td.small-9, + th.small-9 { + display: inline-block !important; + width: 75% !important; + } + td.small-10, + th.small-10 { + display: inline-block !important; + width: 83.33333% !important; + } + td.small-11, + th.small-11 { + display: inline-block !important; + width: 91.66667% !important; + } + td.small-12, + th.small-12 { + display: inline-block !important; + width: 100% !important; + } + .columns td.small-12, + .column td.small-12, + .columns th.small-12, + .column th.small-12 { + display: block !important; + width: 100% !important; + } + table.body td.small-offset-1, + table.body th.small-offset-1 { + margin-left: 8.33333% !important; + Margin-left: 8.33333% !important; + } + table.body td.small-offset-2, + table.body th.small-offset-2 { + margin-left: 16.66667% !important; + Margin-left: 16.66667% !important; + } + table.body td.small-offset-3, + table.body th.small-offset-3 { + margin-left: 25% !important; + Margin-left: 25% !important; + } + table.body td.small-offset-4, + table.body th.small-offset-4 { + margin-left: 33.33333% !important; + Margin-left: 33.33333% !important; + } + table.body td.small-offset-5, + table.body th.small-offset-5 { + margin-left: 41.66667% !important; + Margin-left: 41.66667% !important; + } + table.body td.small-offset-6, + table.body th.small-offset-6 { + margin-left: 50% !important; + Margin-left: 50% !important; + } + table.body td.small-offset-7, + table.body th.small-offset-7 { + margin-left: 58.33333% !important; + Margin-left: 58.33333% !important; + } + table.body td.small-offset-8, + table.body th.small-offset-8 { + margin-left: 66.66667% !important; + Margin-left: 66.66667% !important; + } + table.body td.small-offset-9, + table.body th.small-offset-9 { + margin-left: 75% !important; + Margin-left: 75% !important; + } + table.body td.small-offset-10, + table.body th.small-offset-10 { + margin-left: 83.33333% !important; + Margin-left: 83.33333% !important; + } + table.body td.small-offset-11, + table.body th.small-offset-11 { + margin-left: 91.66667% !important; + Margin-left: 91.66667% !important; + } + table.body table.columns td.expander, + table.body table.columns th.expander { + display: none !important; + } + table.body .right-text-pad, + table.body .text-pad-right { + padding-left: 10px !important; + } + table.body .left-text-pad, + table.body .text-pad-left { + padding-right: 10px !important; + } + table.menu { + width: 100% !important; + } + table.menu td, + table.menu th { + width: auto !important; + display: inline-block !important; + } + table.menu.vertical td, + table.menu.vertical th, + table.menu.small-vertical td, + table.menu.small-vertical th { + display: block !important; + } + table.menu[align="center"] { + width: auto !important; + } + table.button.small-expand, + table.button.small-expanded { + width: 100% !important; + } + table.button.small-expand table, + table.button.small-expanded table { + width: 100%; + } + table.button.small-expand table a, + table.button.small-expanded table a { + text-align: center !important; + width: 100% !important; + padding-left: 0 !important; + padding-right: 0 !important; + } + table.button.small-expand center, + table.button.small-expanded center { + min-width: 0; + } +} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig new file mode 100644 index 0000000000000..2f3a346df5903 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.html.twig @@ -0,0 +1,65 @@ +{% apply inky_to_html|inline_css %} + + + + + + + + + + + + {% block lead %} + {{ importance|upper }} +

+ {{ email.subject }} +

+ {% endblock %} + + {% block content %} + {% if markdown %} + {{ include('@email/zurb_2/notification/content_markdown.html.twig') }} + {% else %} + {{ (raw ? content|raw : content)|nl2br }} + {% endif %} + {% endblock %} + + {% block action %} + {% if action_url %} + + + {% endif %} + {% endblock %} + + {% block exception %} + {% if exception %} + +

Exception stack trace attached.

+ {% endif %} + {% endblock %} +
+
+ + + + {% block footer %} + + + {% block footer_content %} +

Notification e-mail sent by Symfony

+ {% endblock %} +
+
+ {% endblock %} +
+
+
+ + +{% endapply %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig new file mode 100644 index 0000000000000..db855829703e4 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/body.txt.twig @@ -0,0 +1,20 @@ +{% block lead %} +{{ email.subject }} +{% endblock %} + +{% block content %} +{{ content }} +{% endblock %} + +{% block action %} +{% if action_url %} +{{ action_url }}: {{ action_text }} +{% endif %} +{% endblock %} + +{% block exception %} +{% if exception %} +Exception stack trace attached. +{{ exception }} +{% endif %} +{% endblock %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/content_markdown.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/content_markdown.html.twig new file mode 100644 index 0000000000000..120b2caad9623 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/content_markdown.html.twig @@ -0,0 +1 @@ +{{ content|markdown_to_html }} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/local.css b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/local.css new file mode 100644 index 0000000000000..2e68dcd3ef37d --- /dev/null +++ b/src/Symfony/Bridge/Twig/Resources/views/Email/zurb_2/notification/local.css @@ -0,0 +1,19 @@ +body { + background: #f3f3f3; +} + +.wrapper.secondary { + background: #f3f3f3; +} + +.container.body_alert { + border-top: 8px solid #ec5840; +} + +.container.body_warning { + border-top: 8px solid #ffae00; +} + +.container.body_default { + border-top: 8px solid #aaaaaa; +} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig index b082d9236b927..49cd804398b5e 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_horizontal_layout.html.twig @@ -27,7 +27,7 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} @@ -38,7 +38,7 @@ col-sm-2 {%- endblock form_row %} {% block submit_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -47,7 +47,7 @@ col-sm-2 {%- endblock submit_row %} {% block reset_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -60,10 +60,11 @@ col-sm-10 {%- endblock form_group_class %} {% block checkbox_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(form) -}}
{#--#}
diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index a1e2a4f28c7f6..12d9545f3aaff 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -85,9 +85,6 @@ {%- if required -%} {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%} {%- endif -%} - {%- if parent_label_class is defined -%} - {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|trim}) -%} - {%- endif -%} {%- if label is not same as(false) and label is empty -%} {%- if label_format is not empty -%} {%- set label = label_format|replace({ @@ -99,7 +96,7 @@ {%- endif -%} {%- endif -%} - {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans({}, translation_domain)) -}} + {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain)) -}} {%- endif -%} {%- endblock checkbox_radio_label %} @@ -111,7 +108,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) }} {# -#} {{ form_widget(form, widget_attr) }} {# -#} {{- form_help(form) -}} @@ -120,7 +117,7 @@ {%- endblock form_row %} {% block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row %} @@ -146,15 +143,17 @@ {%- endblock datetime_row %} {% block checkbox_row -%} -
+ {{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(form) -}}
{%- endblock checkbox_row %} {% block radio_row -%} -
+ {{- form_widget(form) -}} + {{- form_help(form) -}} {{- form_errors(form) -}}
{%- endblock radio_row %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig index 7fcea4b0ecd25..990b324cb0d17 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_horizontal_layout.html.twig @@ -28,7 +28,7 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} @@ -43,19 +43,20 @@ col-sm-2 {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+
{{- form_label(form) -}}
{{- form_widget(form, widget_attr) -}} {{- form_help(form) -}} + {{- form_errors(form) -}}
{##}
{%- endblock fieldset_form_row %} {% block submit_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -64,7 +65,7 @@ col-sm-2 {%- endblock submit_row %} {% block reset_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} @@ -77,12 +78,11 @@ col-sm-10 {%- endblock form_group_class %} {% block checkbox_row -%} -
{#--#} + {#--#}
{#--#}
{{- form_widget(form) -}} {{- form_help(form) -}} - {{- form_errors(form) -}}
{#--#}
{%- endblock checkbox_row %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index 1848d0dc9838c..f08fc8d20f9cc 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -122,9 +122,9 @@ <{{ element|default('div') }} class="custom-file"> {%- set type = type|default('file') -%} {{- block('form_widget_simple') -}} - {%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%} + {%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim })|filter((value, key) => key != 'id') -%} @@ -132,9 +132,15 @@ {% endblock %} {% block form_widget_simple -%} - {% if type is not defined or type != 'hidden' %} - {%- set attr = attr|merge({class: (attr.class|default('') ~ (type|default('') == 'file' ? ' custom-file-input' : ' form-control'))|trim}) -%} - {% endif %} + {%- if type is not defined or type != 'hidden' -%} + {%- set className = ' form-control' -%} + {%- if type|default('') == 'file' -%} + {%- set className = ' custom-file-input' -%} + {%- elseif type|default('') == 'range' -%} + {%- set className = ' form-control-range' -%} + {%- endif -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ className)|trim}) -%} + {%- endif -%} {%- if type is defined and (type == 'range' or type == 'color') %} {# Attribute "required" is not supported #} {%- set required = false -%} @@ -142,12 +148,12 @@ {{- parent() -}} {%- endblock form_widget_simple %} -{%- block widget_attributes -%} - {%- if not valid %} +{% block widget_attributes -%} + {%- if not valid -%} {% set attr = attr|merge({class: (attr.class|default('') ~ ' is-invalid')|trim}) %} - {% endif -%} + {%- endif -%} {{ parent() }} -{%- endblock widget_attributes -%} +{%- endblock widget_attributes %} {% block button_widget -%} {%- set attr = attr|merge({class: (attr.class|default('btn-secondary') ~ ' btn')|trim}) -%} @@ -166,6 +172,11 @@
{{- form_label(form, null, { widget: parent() }) -}}
+ {%- elseif 'switch-custom' in parent_label_class -%} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' custom-control-input')|trim}) -%} +
+ {{- form_label(form, null, { widget: parent() }) -}} +
{%- else -%} {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-check-input')|trim}) -%}
@@ -237,8 +248,8 @@ {% block checkbox_radio_label -%} {#- Do not display the label if widget is not defined in order to prevent double label rendering -#} {%- if widget is defined -%} - {% set is_parent_custom = parent_label_class is defined and ('checkbox-custom' in parent_label_class or 'radio-custom' in parent_label_class) %} - {% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class) %} + {% set is_parent_custom = parent_label_class is defined and ('checkbox-custom' in parent_label_class or 'radio-custom' in parent_label_class or 'switch-custom' in parent_label_class) %} + {% set is_custom = label_attr.class is defined and ('checkbox-custom' in label_attr.class or 'radio-custom' in label_attr.class or 'switch-custom' in label_attr.class) %} {%- if is_parent_custom or is_custom -%} {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' custom-control-label')|trim}) -%} {%- else %} @@ -250,9 +261,6 @@ {%- if required -%} {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' required')|trim}) -%} {%- endif -%} - {%- if parent_label_class is defined -%} - {%- set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' ' ~ parent_label_class)|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': ''})|trim}) -%} - {%- endif -%} {%- if label is not same as(false) and label is empty -%} {%- if label_format is not empty -%} {%- set label = label_format|replace({ @@ -266,7 +274,7 @@ {{ widget|raw }} - {{- label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans({}, translation_domain)) -}} + {{- label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain)) -}} {{- form_errors(form) -}} {%- endif -%} @@ -282,7 +290,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} - <{{ element|default('div') }} class="form-group"> + <{{ element|default('div') }}{% with {attr: row_attr|merge({class: (row_attr.class|default('') ~ ' form-group')|trim})} %}{{ block('attributes') }}{% endwith %}> {{- form_label(form) -}} {{- form_widget(form, widget_attr) -}} {{- form_help(form) -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig index a6ee019a094b6..1b0092859dbd9 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_base_layout.html.twig @@ -172,7 +172,7 @@ {% block choice_label -%} {# remove the checkbox-inline and radio-inline class, it's only useful for embed labels #} - {%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': ''})|trim}) -%} + {%- set label_attr = label_attr|merge({class: label_attr.class|default('')|replace({'checkbox-inline': '', 'radio-inline': '', 'checkbox-custom': '', 'radio-custom': '', 'switch-custom': ''})|trim}) -%} {{- block('form_label') -}} {% endblock choice_label %} @@ -187,7 +187,7 @@ {# Rows #} {% block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig index 0a5cd42cfda41..066ed89c6372b 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_div_layout.html.twig @@ -65,12 +65,14 @@ {%- endif -%} {%- if preferred_choices|length > 0 -%} {% set options = preferred_choices %} + {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {%- if choices|length > 0 and separator is not none -%} {%- endif -%} {%- endif -%} {%- set options = choices -%} + {%- set render_preferred_choices = false -%} {{- block('choice_widget_options') -}} {%- endblock choice_widget_collapsed -%} @@ -83,7 +85,7 @@ {{- block('choice_widget_options') -}} {%- else -%} - + {%- endif -%} {% endfor %} {%- endblock choice_widget_options -%} @@ -226,13 +228,11 @@ '%name%': name, '%id%': id, }) %} - {%- elseif label is same as(false) -%} - {% set translation_domain = false %} - {%- else -%} + {%- elseif label is not same as(false) -%} {% set label = name|humanize %} {%- endif -%} {%- endif -%} - + {%- endblock button_widget -%} {%- block submit_widget -%} @@ -255,6 +255,17 @@ {{ block('form_widget_simple') }} {%- endblock color_widget -%} +{%- block week_widget -%} + {%- if widget == 'single_text' -%} + {{ block('form_widget_simple') }} + {%- else -%} + {%- set vars = widget == 'text' ? { 'attr': { 'size': 1 }} : {} -%} +
+ {{ form_widget(form.year, vars) }}-{{ form_widget(form.week, vars) }} +
+ {%- endif -%} +{%- endblock week_widget -%} + {# Labels #} {%- block form_label -%} @@ -325,7 +336,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+ {{- form_label(form) -}} {{- form_errors(form) -}} {{- form_widget(form, widget_attr) -}} @@ -334,7 +345,7 @@ {%- endblock form_row -%} {%- block button_row -%} -
+ {{- form_widget(form) -}}
{%- endblock button_row -%} @@ -433,7 +444,7 @@ {%- for attrname, attrvalue in attr -%} {{- " " -}} {%- if attrname in ['placeholder', 'title'] -%} - {{- attrname }}="{{ translation_domain is same as(false) ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain) }}" + {{- attrname }}="{{ translation_domain is same as(false) or attrvalue is null ? attrvalue : attrvalue|trans(attr_translation_parameters, translation_domain) }}" {%- elseif attrvalue is same as(true) -%} {{- attrname }}="{{ attrname }}" {%- elseif attrvalue is not same as(false) -%} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig index 10eaf566d097d..00a51ab04bc28 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/form_table_layout.html.twig @@ -5,7 +5,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} - + {{- form_label(form) -}} @@ -18,7 +18,7 @@ {%- endblock form_row -%} {%- block button_row -%} - + {{- form_widget(form) -}} @@ -27,7 +27,8 @@ {%- endblock button_row -%} {%- block hidden_row -%} - + {%- set style = row_attr.style is defined ? (row_attr.style ~ (row_attr.style|trim|last != ';' ? '; ')) -%} + {{- form_widget(form) -}} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig index 9547ea4900fe6..f8c51b83dd8ed 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/foundation_5_layout.html.twig @@ -160,12 +160,14 @@ {%- endif %} {%- if preferred_choices|length > 0 -%} {% set options = preferred_choices %} + {% set render_preferred_choices = true %} {{- block('choice_widget_options') -}} {% if choices|length > 0 and separator is not none -%} {%- endif %} {%- endif -%} {% set options = choices -%} + {%- set render_preferred_choices = false -%} {{- block('choice_widget_options') -}} {%- endblock choice_widget_collapsed %} @@ -251,9 +253,6 @@ {% if errors|length > 0 -%} {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ ' error')|trim}) %} {% endif %} - {% if parent_label_class is defined %} - {% set label_attr = label_attr|merge({class: (label_attr.class|default('') ~ parent_label_class)|trim}) %} - {% endif %} {% if label is empty %} {%- if label_format is not empty -%} {% set label = label_format|replace({ @@ -266,7 +265,7 @@ {% endif %} {{ widget|raw }} - {{ translation_domain is same as(false) ? label : label|trans({}, translation_domain) }} + {{ translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain) }} {%- endblock checkbox_radio_label %} @@ -277,7 +276,7 @@ {%- if help is not empty -%} {%- set widget_attr = {attr: {'aria-describedby': id ~"_help"}} -%} {%- endif -%} -
+
{{- form_label(form) -}} {{- form_widget(form, widget_attr) -}} @@ -308,18 +307,20 @@ {%- endblock datetime_row %} {% block checkbox_row -%} -
+
{{ form_widget(form) }} + {{- form_help(form) -}} {{ form_errors(form) }}
{%- endblock checkbox_row %} {% block radio_row -%} -
+
{{ form_widget(form) }} + {{- form_help(form) -}} {{ form_errors(form) }}
diff --git a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php index 53b84b2d1bf9e..b496aa9679ad7 100644 --- a/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php +++ b/src/Symfony/Bridge/Twig/Tests/AppVariableTest.php @@ -1,12 +1,25 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\Twig\Tests; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\AppVariable; +use Symfony\Bridge\Twig\Tests\Fixtures\TokenInterface; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; class AppVariableTest extends TestCase { @@ -15,7 +28,7 @@ class AppVariableTest extends TestCase */ protected $appVariable; - protected function setUp() + protected function setUp(): void { $this->appVariable = new AppVariable(); } @@ -50,7 +63,8 @@ public function testEnvironment() */ public function testGetSession() { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); + $request = $this->createMock(Request::class); + $request->method('hasSession')->willReturn(true); $request->method('getSession')->willReturn($session = new Session()); $this->setRequestStack($request); @@ -74,10 +88,10 @@ public function testGetRequest() public function testGetToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->appVariable->setTokenStorage($tokenStorage); - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = $this->createMock(TokenInterface::class); $tokenStorage->method('getToken')->willReturn($token); $this->assertEquals($token, $this->appVariable->getToken()); @@ -85,7 +99,7 @@ public function testGetToken() public function testGetUser() { - $this->setTokenStorage($user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock()); + $this->setTokenStorage($user = $this->createMock(UserInterface::class)); $this->assertEquals($user, $this->appVariable->getUser()); } @@ -99,7 +113,7 @@ public function testGetUserWithUsernameAsTokenUser() public function testGetTokenWithNoToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->appVariable->setTokenStorage($tokenStorage); $this->assertNull($this->appVariable->getToken()); @@ -107,57 +121,45 @@ public function testGetTokenWithNoToken() public function testGetUserWithNoToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->appVariable->setTokenStorage($tokenStorage); $this->assertNull($this->appVariable->getUser()); } - /** - * @expectedException \RuntimeException - */ public function testEnvironmentNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getEnvironment(); } - /** - * @expectedException \RuntimeException - */ public function testDebugNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getDebug(); } - /** - * @expectedException \RuntimeException - */ public function testGetTokenWithTokenStorageNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getToken(); } - /** - * @expectedException \RuntimeException - */ public function testGetUserWithTokenStorageNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getUser(); } - /** - * @expectedException \RuntimeException - */ public function testGetRequestWithRequestStackNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getRequest(); } - /** - * @expectedException \RuntimeException - */ public function testGetSessionWithRequestStackNotSet() { + $this->expectException(\RuntimeException::class); $this->appVariable->getSession(); } @@ -191,10 +193,10 @@ public function testGetFlashes() $flashMessages = $this->setFlashMessages(); $this->assertEquals($flashMessages, $this->appVariable->getFlashes([])); - $flashMessages = $this->setFlashMessages(); + $this->setFlashMessages(); $this->assertEquals([], $this->appVariable->getFlashes('this-does-not-exist')); - $flashMessages = $this->setFlashMessages(); + $this->setFlashMessages(); $this->assertEquals( ['this-does-not-exist' => []], $this->appVariable->getFlashes(['this-does-not-exist']) @@ -235,7 +237,7 @@ public function testGetFlashes() protected function setRequestStack($request) { - $requestStackMock = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStackMock = $this->createMock(RequestStack::class); $requestStackMock->method('getCurrentRequest')->willReturn($request); $this->appVariable->setRequestStack($requestStackMock); @@ -243,10 +245,10 @@ protected function setRequestStack($request) protected function setTokenStorage($user) { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->appVariable->setTokenStorage($tokenStorage); - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = $this->createMock(TokenInterface::class); $tokenStorage->method('getToken')->willReturn($token); $token->method('getUser')->willReturn($user); @@ -262,11 +264,12 @@ private function setFlashMessages($sessionHasStarted = true) $flashBag = new FlashBag(); $flashBag->initialize($flashMessages); - $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')->disableOriginalConstructor()->getMock(); + $session = $this->createMock(Session::class); $session->method('isStarted')->willReturn($sessionHasStarted); $session->method('getFlashBag')->willReturn($flashBag); - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); + $request = $this->createMock(Request::class); + $request->method('hasSession')->willReturn(true); $request->method('getSession')->willReturn($session); $this->setRequestStack($request); diff --git a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php index f6a52c2e02e91..9fa1af4759eab 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/DebugCommandTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Command\DebugCommand; use Symfony\Component\Console\Application; +use Symfony\Component\Console\Exception\InvalidArgumentException; use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\ChainLoader; @@ -27,7 +28,7 @@ public function testDebugCommand() $ret = $tester->execute([], ['decorated' => false]); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('Functions', trim($tester->getDisplay())); + $this->assertStringContainsString('Functions', trim($tester->getDisplay())); } public function testFilterAndJsonFormatOptions() @@ -89,12 +90,10 @@ public function testDeprecationForWrongBundleOverridingInLegacyPath() $this->assertEquals($expected, json_decode($tester->getDisplay(true), true)); } - /** - * @expectedException \Symfony\Component\Console\Exception\InvalidArgumentException - * @expectedExceptionMessage Malformed namespaced template name "@foo" (expecting "@namespace/template_name"). - */ public function testMalformedTemplateName() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Malformed namespaced template name "@foo" (expecting "@namespace/template_name").'); $this->createCommandTester()->execute(['name' => '@foo']); } @@ -286,7 +285,7 @@ public function testDebugTemplateNameWithChainLoader() $ret = $tester->execute(['name' => 'base.html.twig'], ['decorated' => false]); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('[OK]', $tester->getDisplay()); + $this->assertStringContainsString('[OK]', $tester->getDisplay()); } public function testWithGlobals() @@ -295,7 +294,7 @@ public function testWithGlobals() $tester = $this->createCommandTester([], [], null, null, false, ['message' => $message]); $tester->execute([], ['decorated' => true]); $display = $tester->getDisplay(); - $this->assertContains(json_encode($message), $display); + $this->assertStringContainsString(json_encode($message), $display); } public function testWithGlobalsJson() diff --git a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php index 82b71eed870cc..9bb9a9867c745 100644 --- a/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Command/LintCommandTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Tester\CommandTester; use Twig\Environment; use Twig\Loader\FilesystemLoader; +use Twig\TwigFilter; class LintCommandTest extends TestCase { @@ -31,7 +32,7 @@ public function testLintCorrectFile() $ret = $tester->execute(['filename' => [$filename]], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('OK in', trim($tester->getDisplay())); + $this->assertStringContainsString('OK in', trim($tester->getDisplay())); } public function testLintIncorrectFile() @@ -42,19 +43,17 @@ public function testLintIncorrectFile() $ret = $tester->execute(['filename' => [$filename]], ['decorated' => false]); $this->assertEquals(1, $ret, 'Returns 1 in case of error'); - $this->assertRegExp('/ERROR in \S+ \(line /', trim($tester->getDisplay())); + $this->assertMatchesRegularExpression('/ERROR in \S+ \(line /', trim($tester->getDisplay())); } - /** - * @expectedException \RuntimeException - */ public function testLintFileNotReadable() { + $this->expectException(\RuntimeException::class); $tester = $this->createCommandTester(); $filename = $this->createFile(''); unlink($filename); - $ret = $tester->execute(['filename' => [$filename]], ['decorated' => false]); + $tester->execute(['filename' => [$filename]], ['decorated' => false]); } public function testLintFileCompileTimeException() @@ -65,15 +64,57 @@ public function testLintFileCompileTimeException() $ret = $tester->execute(['filename' => [$filename]], ['decorated' => false]); $this->assertEquals(1, $ret, 'Returns 1 in case of error'); - $this->assertRegExp('/ERROR in \S+ \(line /', trim($tester->getDisplay())); + $this->assertMatchesRegularExpression('/ERROR in \S+ \(line /', trim($tester->getDisplay())); } /** - * @return CommandTester + * When deprecations are not reported by the command, the testsuite reporter will catch them so we need to mark the test as legacy. + * + * @group legacy */ - private function createCommandTester() + public function testLintFileWithNotReportedDeprecation() { - $command = new LintCommand(new Environment(new FilesystemLoader())); + $tester = $this->createCommandTester(); + $filename = $this->createFile('{{ foo|deprecated_filter }}'); + + $ret = $tester->execute(['filename' => [$filename]], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('OK in', trim($tester->getDisplay())); + } + + public function testLintFileWithReportedDeprecation() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('{{ foo|deprecated_filter }}'); + + $ret = $tester->execute(['filename' => [$filename], '--show-deprecations' => true], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]); + + $this->assertEquals(1, $ret, 'Returns 1 in case of error'); + $this->assertMatchesRegularExpression('/ERROR in \S+ \(line 1\)/', trim($tester->getDisplay())); + $this->assertStringContainsString('Filter "deprecated_filter" is deprecated', trim($tester->getDisplay())); + } + + /** + * @group tty + */ + public function testLintDefaultPaths() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute([], ['verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false]); + + $this->assertEquals(0, $ret, 'Returns 0 in case of success'); + self::assertStringContainsString('OK in', trim($tester->getDisplay())); + } + + private function createCommandTester(): CommandTester + { + $environment = new Environment(new FilesystemLoader(\dirname(__DIR__).'/Fixtures/templates/')); + $environment->addFilter(new TwigFilter('deprecated_filter', function ($v) { + return $v; + }, ['deprecated' => true])); + + $command = new LintCommand($environment); $application = new Application(); $application->add($command); @@ -82,10 +123,7 @@ private function createCommandTester() return new CommandTester($command); } - /** - * @return string Path to the new file - */ - private function createFile($content) + private function createFile($content): string { $filename = tempnam(sys_get_temp_dir(), 'sf-'); file_put_contents($filename, $content); @@ -95,16 +133,16 @@ private function createFile($content) return $filename; } - protected function setUp() + protected function setUp(): void { $this->files = []; } - protected function tearDown() + protected function tearDown(): void { foreach ($this->files as $file) { if (file_exists($file)) { - unlink($file); + @unlink($file); } } } diff --git a/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php b/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.php new file mode 100644 index 0000000000000..9febc61e61887 --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/ErrorRenderer/TwigErrorRendererTest.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\Bridge\Twig\Tests\ErrorRenderer; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\Exception\FlattenException; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Twig\Environment; +use Twig\Loader\ArrayLoader; + +class TwigErrorRendererTest extends TestCase +{ + public function testFallbackToNativeRendererIfDebugOn() + { + $exception = new \Exception(); + + $twig = $this->createMock(Environment::class); + $nativeRenderer = $this->createMock(HtmlErrorRenderer::class); + $nativeRenderer + ->expects($this->once()) + ->method('render') + ->with($exception) + ; + + (new TwigErrorRenderer($twig, $nativeRenderer, true))->render(new \Exception()); + } + + public function testFallbackToNativeRendererIfCustomTemplateNotFound() + { + $exception = new NotFoundHttpException(); + + $twig = new Environment(new ArrayLoader([])); + + $nativeRenderer = $this->createMock(HtmlErrorRenderer::class); + $nativeRenderer + ->expects($this->once()) + ->method('render') + ->with($exception) + ->willReturn(FlattenException::createFromThrowable($exception)) + ; + + (new TwigErrorRenderer($twig, $nativeRenderer, false))->render($exception); + } + + public function testRenderCustomErrorTemplate() + { + $twig = new Environment(new ArrayLoader([ + '@Twig/Exception/error404.html.twig' => '

Page Not Found

', + ])); + $exception = (new TwigErrorRenderer($twig))->render(new NotFoundHttpException()); + + $this->assertSame('

Page Not Found

', $exception->getAsString()); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php index 9131216182a3d..05fe771254894 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3HorizontalLayoutTest.php @@ -21,7 +21,7 @@ public function testLabelOnForm() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/label + '/label [@class="col-sm-2 control-label required"] [.="[trans]Name[/trans]"] ' @@ -38,7 +38,7 @@ public function testLabelDoesNotRenderFieldAttributes() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="col-sm-2 control-label required"] ' @@ -55,7 +55,7 @@ public function testLabelWithCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-sm-2 control-label required"] ' @@ -72,7 +72,7 @@ publ 10000 ic function testLabelWithCustomTextAndCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-sm-2 control-label required"] [.="[trans]Custom label[/trans]"] @@ -92,7 +92,7 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-sm-2 control-label required"] [.="[trans]Custom label[/trans]"] @@ -163,4 +163,23 @@ public function testCheckboxRow() $this->assertMatchesXpath($html, '/div[@class="form-group"]/div[@class="col-sm-2" or @class="col-sm-10"]', 2); } + + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] + ] +' + ); + } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php index b332ff018d742..7c06163806af0 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap3LayoutTest.php @@ -17,8 +17,6 @@ abstract class AbstractBootstrap3LayoutTest extends AbstractLayoutTest { - protected static $supportedFeatureSetVersion = 403; - public function testLabelOnForm() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\DateType'); @@ -27,7 +25,7 @@ public function testLabelOnForm() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/label + '/label [@class="control-label required"] [.="[trans]Name[/trans]"] ' @@ -44,7 +42,7 @@ public function testLabelDoesNotRenderFieldAttributes() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="control-label required"] ' @@ -61,7 +59,7 @@ public function testLabelWithCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class control-label required"] ' @@ -78,7 +76,7 @@ public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class control-label required"] [.="[trans]Custom label[/trans]"] @@ -98,7 +96,7 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class control-label required"] [.="[trans]Custom label[/trans]"] @@ -115,7 +113,7 @@ public function testHelp() $html = $this->renderHelp($view); $this->assertMatchesXpath($html, -'/span + '/span [@id="name_help"] [@class="help-block"] [.="[trans]Help text test![/trans]"] @@ -235,7 +233,7 @@ public function testErrors() $html = $this->renderErrors($view); $this->assertMatchesXpath($html, -'/div + '/div [@class="alert alert-danger"] [ ./ul @@ -265,7 +263,7 @@ public function testOverrideWidgetBlock() $html = $this->renderWidget($form->createView()); $this->assertMatchesXpath($html, -'/div + '/div [ ./input [@type="text"] @@ -282,7 +280,7 @@ public function testCheckedCheckbox() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', true); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="checkbox"] [ ./label @@ -300,7 +298,7 @@ public function testUncheckedCheckbox() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType', false); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="checkbox"] [ ./label @@ -320,7 +318,7 @@ public function testCheckboxWithValue() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="checkbox"] [ ./label @@ -333,6 +331,21 @@ public function testCheckboxWithValue() ); } + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testSingleChoice() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ @@ -342,7 +355,7 @@ public function testSingleChoice() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -365,7 +378,7 @@ public function testSingleChoiceAttributesWithMainAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], -'/select + '/select [@name="name"] [@class="bar&baz form-control"] [not(@required)] @@ -388,7 +401,7 @@ public function testSingleExpandedChoiceAttributesWithMainAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], -'/div + '/div [@class="bar&baz"] [ ./div @@ -425,7 +438,7 @@ public function testSelectWithSizeBiggerThanOneCanBeRequired() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => '']], -'/select + '/select [@name="name"] [@required="required"] [@size="2"] @@ -444,7 +457,7 @@ public function testSingleChoiceWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -469,7 +482,7 @@ public function testSingleChoiceWithPlaceholderWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -493,7 +506,7 @@ public function testSingleChoiceAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -516,7 +529,7 @@ public function testSingleChoiceWithPreferred() ]); $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -524,8 +537,34 @@ public function testSingleChoiceWithPreferred() ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] ] - [count(./option)=3] + [count(./option)=4] +' + ); + } + + public function testSingleChoiceWithSelectedPreferred() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\ChoiceType', '&a', [ + 'choices' => ['Choice&A' => '&a', 'Choice&B' => '&b'], + 'preferred_choices' => ['&a'], + 'multiple' => false, + 'expanded' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '-- sep --', 'attr' => ['class' => 'my&class']], + '/select + [@name="name"] + [@class="my&class form-control"] + [not(@required)] + [ + ./option[@value="&a"][not(@selected)][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@disabled="disabled"][not(@selected)][.="-- sep --"] + /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] + ] + [count(./option)=4] ' ); } @@ -540,15 +579,16 @@ public function testSingleChoiceWithPreferredAndNoSeparator() ]); $this->assertWidgetMatchesXpath($form->createView(), ['separator' => null, 'attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] [ ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] ] - [count(./option)=2] + [count(./option)=3] ' ); } @@ -563,7 +603,7 @@ public function testSingleChoiceWithPreferredAndBlankSeparator() ]); $this->assertWidgetMatchesXpath($form->createView(), ['separator' => '', 'attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -571,8 +611,9 @@ public function testSingleChoiceWithPreferredAndBlankSeparator() ./option[@value="&b"][not(@selected)][.="[trans]Choice&B[/trans]"] /following-sibling::option[@disabled="disabled"][not(@selected)][.=""] /following-sibling::option[@value="&a"][@selected="selected"][.="[trans]Choice&A[/trans]"] + /following-sibling::option[@value="&b"][.="[trans]Choice&B[/trans]"] ] - [count(./option)=3] + [count(./option)=4] ' ); } @@ -587,9 +628,9 @@ public function testChoiceWithOnlyPreferred() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@class="my&class form-control"] - [count(./option)=2] + [count(./option)=5] ' ); } @@ -604,7 +645,7 @@ public function testSingleChoiceNonRequired() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -628,7 +669,7 @@ public function testSingleChoiceNonRequiredNoneSelected() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -653,7 +694,7 @@ public function testSingleChoiceNonRequiredWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -678,7 +719,7 @@ public function testSingleChoiceRequiredWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [@required="required"] @@ -702,7 +743,7 @@ public function testSingleChoiceRequiredWithPlaceholderViaView() ]); $this->assertWidgetMatchesXpath($form->createView(), ['placeholder' => '', 'attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [@required="required"] @@ -728,7 +769,7 @@ public function testSingleChoiceGrouped() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [./optgroup[@label="[trans]Group&1[/trans]"] @@ -757,7 +798,7 @@ public function testMultipleChoice() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name[]"] [@class="my&class form-control"] [@required="required"] @@ -782,7 +823,7 @@ public function testMultipleChoiceAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name[]"] [@class="my&class form-control"] [@required="required"] @@ -806,7 +847,7 @@ public function testMultipleChoiceSkipsPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name[]"] [@class="my&class form-control"] [@multiple="multiple"] @@ -829,7 +870,7 @@ public function testMultipleChoiceNonRequired() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name[]"] [@class="my&class form-control"] [@multiple="multiple"] @@ -851,7 +892,7 @@ public function testSingleChoiceExpanded() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -887,7 +928,7 @@ public function testSingleChoiceExpandedWithLabelsAsFalse() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -927,7 +968,7 @@ public function testSingleChoiceExpandedWithLabelsSetByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -973,7 +1014,7 @@ public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1007,7 +1048,7 @@ public function testSingleChoiceExpandedWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1043,7 +1084,7 @@ public function testSingleChoiceExpandedAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1080,7 +1121,7 @@ public function testSingleChoiceExpandedWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1127,7 +1168,7 @@ public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1171,7 +1212,7 @@ public function testSingleChoiceExpandedWithBooleanValue() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="radio"] @@ -1207,7 +1248,7 @@ public function testMultipleChoiceExpanded() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="checkbox"] @@ -1252,7 +1293,7 @@ public function testMultipleChoiceExpandedWithLabelsAsFalse() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="checkbox"] @@ -1338,7 +1379,7 @@ public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="checkbox"] @@ -1373,7 +1414,7 @@ public function testMultipleChoiceExpandedWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="checkbox"] @@ -1419,7 +1460,7 @@ public function testMultipleChoiceExpandedAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="checkbox"] @@ -1459,7 +1500,7 @@ public function testCountry() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CountryType', 'AT'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [./option[@value="AT"][@selected="selected"][.="Austria"]] @@ -1476,7 +1517,7 @@ public function testCountryWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Country[/trans]"]] @@ -1494,7 +1535,7 @@ public function testDateTime() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [ ./select [@id="name_date_month"] @@ -1531,7 +1572,7 @@ public function testDateTimeWithPlaceholderGlobal() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1570,7 +1611,7 @@ public function testDateTimeWithHourAndMinute() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1607,7 +1648,7 @@ public function testDateTimeWithSeconds() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1649,7 +1690,7 @@ public function testDateTimeSingleText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./input @@ -1679,7 +1720,7 @@ public function testDateTimeWithWidgetSingleText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="datetime-local"] [@name="name"] [@class="my&class form-control"] @@ -1703,7 +1744,7 @@ public function testDateTimeWithWidgetSingleTextIgnoreDateAndTimeWidgets() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="datetime-local"] [@name="name"] [@class="my&class form-control"] @@ -1720,7 +1761,7 @@ public function testDateChoice() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1751,7 +1792,7 @@ public function testDateChoiceWithPlaceholderGlobal() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1782,7 +1823,7 @@ public function testDateChoiceWithPlaceholderOnYear() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1811,7 +1852,7 @@ public function testDateText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./input @@ -1843,7 +1884,7 @@ public function testDateSingleText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="date"] [@name="name"] [@class="my&class form-control"] @@ -1859,7 +1900,7 @@ public function testBirthDay() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1889,7 +1930,7 @@ public function testBirthDayWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -1918,7 +1959,7 @@ public function testEmail() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\EmailType', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="email"] [@name="name"] [@class="my&class form-control"] @@ -1935,7 +1976,7 @@ public function testEmailWithMaxLength() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="email"] [@name="name"] [@class="my&class form-control"] @@ -1950,7 +1991,7 @@ public function testHidden() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\HiddenType', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="hidden"] [@name="name"] [@class="my&class"] @@ -1966,7 +2007,7 @@ public function testDisabled() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] @@ -1980,7 +2021,7 @@ public function testInteger() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\IntegerType', 123); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="number"] [@name="name"] [@class="my&class form-control"] @@ -1996,7 +2037,7 @@ public function testIntegerTypeWithGroupingRendersAsTextInput() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] @@ -2010,7 +2051,7 @@ public function testLanguage() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\LanguageType', 'de'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [./option[@value="de"][@selected="selected"][.="German"]] @@ -2024,7 +2065,7 @@ public function testLocale() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\LocaleType', 'de_AT'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [./option[@value="de_AT"][@selected="selected"][.="German (Austria)"]] @@ -2040,7 +2081,7 @@ public function testMoney() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./span @@ -2064,7 +2105,7 @@ public function testMoneyWithoutCurrency() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/input + '/input [@id="my&id"] [@type="text"] [@name="name"] @@ -2081,7 +2122,7 @@ public function testNumber() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\NumberType', 1234.56); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] @@ -2111,7 +2152,7 @@ public function testPassword() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PasswordType', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="password"] [@name="name"] [@class="my&class form-control"] @@ -2127,7 +2168,7 @@ public function testPasswordSubmittedWithNotAlwaysEmpty() $form->submit('foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="password"] [@name="name"] [@class="my&class form-control"] @@ -2143,7 +2184,7 @@ public function testPasswordWithMaxLength() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="password"] [@name="name"] [@class="my&class form-control"] @@ -2157,7 +2198,7 @@ public function testPercent() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\PercentType', 0.1); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./input @@ -2178,7 +2219,7 @@ public function testPercentNoSymbol() { $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/input + '/input [@id="my&id"] [@type="text"] [@name="name"] @@ -2192,7 +2233,7 @@ public function testPercentCustomSymbol() { $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => 'β€±']); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./input @@ -2214,7 +2255,7 @@ public function testCheckedRadio() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', true); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="radio"] [ ./label @@ -2238,7 +2279,7 @@ public function testUncheckedRadio() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="radio"] [ ./label @@ -2263,7 +2304,7 @@ public function testRadioWithValue() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="radio"] [ ./label @@ -2281,12 +2322,27 @@ public function testRadioWithValue() ); } + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./span[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testRange() { $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RangeType', 42, ['attr' => ['min' => 5]]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="range"] [@name="name"] [@value="42"] @@ -2301,7 +2357,7 @@ public function testRangeWithMinMaxValues() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RangeType', 42, ['attr' => ['min' => 5, 'max' => 57]]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="range"] [@name="name"] [@value="42"] @@ -2319,9 +2375,9 @@ public function testTextarea() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/textarea + '/textarea [@name="name"] - [@pattern="foo"] + [not(@pattern)] [@class="my&class form-control"] [.="foo&bar"] ' @@ -2333,7 +2389,7 @@ public function testText() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TextType', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] @@ -2350,7 +2406,7 @@ public function testTextWithMaxLength() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] @@ -2365,7 +2421,7 @@ public function testSearch() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\SearchType', 'foo&bar'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="search"] [@name="name"] [@class="my&class form-control"] @@ -2383,7 +2439,7 @@ public function testTime() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -2410,7 +2466,7 @@ public function testTimeWithSeconds() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -2445,7 +2501,7 @@ public function testTimeText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./input @@ -2478,7 +2534,7 @@ public function testTimeSingleText() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="time"] [@name="name"] [@class="my&class form-control"] @@ -2497,7 +2553,7 @@ public function testTimeWithPlaceholderGlobal() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -2524,7 +2580,7 @@ public function testTimeWithPlaceholderOnYear() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/div + '/div [@class="my&class form-inline"] [ ./select @@ -2547,7 +2603,7 @@ public function testTimezone() $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\TimezoneType', 'Europe/Vienna'); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@name="name"] [@class="my&class form-control"] [not(@required)] @@ -2565,7 +2621,7 @@ public function testTimezoneWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/select + '/select [@class="my&class form-control"] [./option[@value=""][not(@selected)][not(@disabled)][.="[trans]Select&Timezone[/trans]"]] [count(.//option)>201] @@ -2575,15 +2631,15 @@ public function testTimezoneWithPlaceholder() public function testUrlWithDefaultProtocol() { - $url = 'http://www.google.com?foo1=bar1&foo2=bar2'; + $url = 'http://www.example.com?foo1=bar1&foo2=bar2'; $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\UrlType', $url, ['default_protocol' => 'http']); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], -'/input + '/input [@type="text"] [@name="name"] [@class="my&class form-control"] - [@value="http://www.google.com?foo1=bar1&foo2=bar2"] + [@value="http://www.example.com?foo1=bar1&foo2=bar2"] [@inputmode="url"] ' ); @@ -2591,7 +2647,7 @@ public function testUrlWithDefaultProtocol() public function testUrlWithoutDefaultProtocol() { - $url = 'http://www.google.com?foo1=bar1&foo2=bar2'; + $url = 'http://www.example.com?foo1=bar1&foo2=bar2'; $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\UrlType', $url, ['default_protocol' => null]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], @@ -2599,7 +2655,7 @@ public function testUrlWithoutDefaultProtocol() [@type="url"] [@name="name"] [@class="my&class form-control"] - [@value="http://www.google.com?foo1=bar1&foo2=bar2"] + [@value="http://www.example.com?foo1=bar1&foo2=bar2"] ' ); } @@ -2719,6 +2775,104 @@ public function testColor() [@name="name"] [@class="my&class form-control"] [@value="#0000ff"] +' + ); + } + + public function testWeekSingleText() + { + $this->requiresFeatureSet(404); + + $form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [ + 'input' => 'string', + 'widget' => 'single_text', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/input + [@type="week"] + [@name="holidays"] + [@class="my&class form-control"] + [@value="1970-W01"] + [not(@maxlength)] +' + ); + } + + public function testWeekSingleTextNoHtml5() + { + $this->requiresFeatureSet(404); + + $form = $this->factory->createNamed('holidays', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '1970-W01', [ + 'input' => 'string', + 'widget' => 'single_text', + 'html5' => false, + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/input + [@type="text"] + [@name="holidays"] + [@class="my&class form-control"] + [@value="1970-W01"] + [not(@maxlength)] +' + ); + } + + public function testWeekChoices() + { + $this->requiresFeatureSet(404); + + $data = ['year' => (int) date('Y'), 'week' => 1]; + + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', $data, [ + 'input' => 'array', + 'widget' => 'choice', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./select + [@id="name_year"] + [@class="form-control"] + [./option[@value="'.$data['year'].'"][@selected="selected"]] + /following-sibling::select + [@id="name_week"] + [@class="form-control"] + [./option[@value="'.$data['week'].'"][@selected="selected"]] + ] + [count(.//select)=2]' + ); + } + + public function testWeekText() + { + $this->requiresFeatureSet(404); + + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\WeekType', '2000-W01', [ + 'input' => 'string', + 'widget' => 'text', + ]); + + $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'my&class']], + '/div + [@class="my&class"] + [ + ./input + [@id="name_year"] + [@type="number"] + [@class="form-control"] + [@value="2000"] + /following-sibling::input + [@id="name_week"] + [@type="number"] + [@class="form-control"] + [@value="1"] + ] + [count(./input)=2] ' ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php index e9416b02213f2..51aa10e416d88 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4HorizontalLayoutTest.php @@ -53,7 +53,7 @@ public function testLabelOnForm() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/legend + '/legend [@class="col-form-label col-sm-2 col-form-label required"] [.="[trans]Name[/trans]"] ' @@ -70,7 +70,7 @@ public function testLabelDoesNotRenderFieldAttributes() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="col-form-label col-sm-2 required"] ' @@ -87,7 +87,7 @@ public function testLabelWithCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-form-label col-sm-2 required"] ' @@ -104,7 +104,7 @@ public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-form-label col-sm-2 required"] [.="[trans]Custom label[/trans]"] @@ -124,7 +124,7 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class col-form-label col-sm-2 required"] [.="[trans]Custom label[/trans]"] @@ -144,7 +144,7 @@ public function testLegendOnExpandedType() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/legend + '/legend [@class="col-sm-2 col-form-label required"] [.="[trans]Custom label[/trans]"] ' @@ -222,7 +222,26 @@ public function testCheckboxRowWithHelp() $html = $this->renderRow($view, ['label' => 'foo', 'help' => 'really helpful text']); $this->assertMatchesXpath($html, -'/div + '/div + [@class="form-group row"] + [ + ./div[@class="col-sm-2" or @class="col-sm-10"] + /following-sibling::div[@class="col-sm-2" or @class="col-sm-10"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] + ] +' + ); + } + + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div [@class="form-group row"] [ ./div[@class="col-sm-2" or @class="col-sm-10"] diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php index 89fbacf2fc2d7..3e02351c4f9f4 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/AbstractBootstrap4LayoutTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\Extension\Core\Type\MoneyType; use Symfony\Component\Form\Extension\Core\Type\PercentType; use Symfony\Component\Form\Extension\Core\Type\RadioType; +use Symfony\Component\Form\Extension\Core\Type\RangeType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormError; @@ -62,7 +63,7 @@ public function testLabelOnForm() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/legend + '/legend [@class="col-form-label required"] [.="[trans]Name[/trans]"] ' @@ -79,7 +80,7 @@ public function testLabelDoesNotRenderFieldAttributes() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="required"] ' @@ -96,7 +97,7 @@ public function testLabelWithCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class required"] ' @@ -113,7 +114,7 @@ public function testLabelWithCustomTextAndCustomAttributesPassedDirectly() ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class required"] [.="[trans]Custom label[/trans]"] @@ -133,7 +134,7 @@ public function testLabelWithCustomTextAsOptionAndCustomAttributesPassedDirectly ]); $this->assertMatchesXpath($html, -'/label + '/label [@for="name"] [@class="my&class required"] [.="[trans]Custom label[/trans]"] @@ -153,7 +154,7 @@ public function testLegendOnExpandedType() $html = $this->renderLabel($view); $this->assertMatchesXpath($html, -'/legend + '/legend [@class="col-form-label required"] [.="[trans]Custom label[/trans]"] ' @@ -169,7 +170,7 @@ public function testHelp() $html = $this->renderHelp($view); $this->assertMatchesXpath($html, -'/small + '/small [@id="name_help"] [@class="form-text text-muted"] [.="[trans]Help text test![/trans]"] @@ -289,7 +290,7 @@ public function testErrors() $html = $this->renderErrors($view); $this->assertMatchesXpath($html, -'/span + '/span [@class="alert alert-danger d-block"] [ ./span[@class="d-block"] @@ -320,7 +321,7 @@ public function testCheckedCheckbox() $form = $this->factory->createNamed('name', CheckboxType::class, true); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class form-check-input"][@checked="checked"][@value="1"] @@ -342,7 +343,7 @@ public function testSingleChoiceAttributesWithMainAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], -'/select + '/select [@name="name"] [@class="bar&baz form-control"] [not(@required)] @@ -365,7 +366,7 @@ public function testSingleExpandedChoiceAttributesWithMainAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), ['attr' => ['class' => 'bar&baz']], -'/div + '/div [@class="bar&baz"] [ ./div @@ -393,7 +394,7 @@ public function testUncheckedCheckbox() $form = $this->factory->createNamed('name', CheckboxType::class, false); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class form-check-input"][not(@checked)] @@ -411,7 +412,7 @@ public function testCheckboxWithValue() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input[@type="checkbox"][@name="name"][@id="my&id"][@class="my&class form-check-input"][@value="foo&bar"] @@ -422,6 +423,21 @@ public function testCheckboxWithValue() ); } + public function testCheckboxRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\CheckboxType'); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testSingleChoiceExpanded() { $form = $this->factory->createNamed('name', ChoiceType::class, '&a', [ @@ -431,7 +447,7 @@ public function testSingleChoiceExpanded() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -463,7 +479,7 @@ public function testSingleChoiceExpandedWithLabelsAsFalse() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -499,7 +515,7 @@ public function testSingleChoiceExpandedWithLabelsSetByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -539,7 +555,7 @@ public function testSingleChoiceExpandedWithLabelsSetFalseByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -569,7 +585,7 @@ public function testSingleChoiceExpandedWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -601,7 +617,7 @@ public function testSingleChoiceExpandedAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -634,7 +650,7 @@ public function testSingleChoiceExpandedWithPlaceholder() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -675,7 +691,7 @@ public function testSingleChoiceExpandedWithPlaceholderWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -713,7 +729,7 @@ public function testSingleChoiceExpandedWithBooleanValue() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -745,7 +761,7 @@ public function testMultipleChoiceExpanded() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -784,7 +800,7 @@ public function testMultipleChoiceExpandedWithLabelsAsFalse() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -860,7 +876,7 @@ public function testMultipleChoiceExpandedWithLabelsSetFalseByCallable() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -891,7 +907,7 @@ public function testMultipleChoiceExpandedWithoutTranslation() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -931,7 +947,7 @@ public function testMultipleChoiceExpandedAttributes() ]); $this->assertWidgetMatchesXpath($form->createView(), [], -'/div + '/div [ ./div [@class="form-check"] @@ -965,7 +981,7 @@ public function testCheckedRadio() $form = $this->factory->createNamed('name', RadioType::class, true); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input @@ -987,7 +1003,7 @@ public function testUncheckedRadio() $form = $this->factory->createNamed('name', RadioType::class, false); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input @@ -1010,7 +1026,7 @@ public function testRadioWithValue() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="form-check"] [ ./input @@ -1027,6 +1043,21 @@ public function testRadioWithValue() ); } + public function testRadioRowWithHelp() + { + $form = $this->factory->createNamed('name', 'Symfony\Component\Form\Extension\Core\Type\RadioType', false); + $html = $this->renderRow($form->createView(), ['label' => 'foo', 'help' => 'really helpful text']); + + $this->assertMatchesXpath($html, + '/div + [@class="form-group"] + [ + ./small[text() = "[trans]really helpful text[/trans]"] + ] +' + ); + } + public function testButtonAttributeNameRepeatedIfTrue() { $form = $this->factory->createNamed('button', ButtonType::class, null, [ @@ -1044,7 +1075,7 @@ public function testFile() $form = $this->factory->createNamed('name', FileType::class); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'n/a', 'attr' => ['class' => 'my&class form-control-file']], -'/div + '/div [@class="custom-file"] [ ./input @@ -1057,12 +1088,30 @@ public function testFile() ); } + public function testFileLabelIdNotDuplicated() + { + $form = $this->factory->createNamed('name', FileType::class); + + $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'n/a', 'attr' => ['class' => 'my&class form-control-file'], 'label_attr' => ['id' => 'label-id']], + '/div + [@class="custom-file"] + [ + ./input + [@type="file"] + [@name="name"] + /following-sibling::label + [@for="name"][not(@id)] + ] +' + ); + } + public function testFileWithPlaceholder() { $form = $this->factory->createNamed('name', FileType::class); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'n/a', 'attr' => ['class' => 'my&class form-control-file', 'placeholder' => 'Custom Placeholder']], -'/div + '/div [@class="custom-file"] [ ./input @@ -1082,7 +1131,7 @@ public function testMoney() ]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./div @@ -1108,7 +1157,7 @@ public function testPercent() $form = $this->factory->createNamed('name', PercentType::class, 0.1); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./input @@ -1133,7 +1182,7 @@ public function testPercentNoSymbol() { $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => false]); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/input + '/input [@id="my&id"] [@type="text"] [@name="name"] @@ -1147,7 +1196,7 @@ public function testPercentCustomSymbol() { $form = $this->factory->createNamed('name', PercentType::class, 0.1, ['symbol' => 'β€±']); $this->assertWidgetMatchesXpath($form->createView(), ['id' => 'my&id', 'attr' => ['class' => 'my&class']], -'/div + '/div [@class="input-group"] [ ./input @@ -1164,6 +1213,41 @@ public function testPercentCustomSymbol() [contains(.., "β€±")] ] ] +' + ); + } + + public function testRange() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5]]); + + $this->assertWidgetMatchesXpath( + $form->createView(), + ['attr' => ['class' => 'my&class']], + '/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@class="my&class form-control-range"] +' + ); + } + + public function testRangeWithMinMaxValues() + { + $form = $this->factory->createNamed('name', RangeType::class, 42, ['attr' => ['min' => 5, 'max' => 57]]); + + $this->assertWidgetMatchesXpath( + $form->createView(), + ['attr' => ['class' => 'my&class']], + '/input + [@type="range"] + [@name="name"] + [@value="42"] + [@min="5"] + [@max="57"] + [@class="my&class form-control-range"] ' ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php index 874faeeb99955..fc0891e118810 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/CodeExtensionTest.php @@ -14,6 +14,8 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\CodeExtension; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Twig\Environment; +use Twig\Loader\ArrayLoader; class CodeExtensionTest extends TestCase { @@ -28,38 +30,123 @@ public function testFileRelative() $this->assertEquals('file.txt', $this->getExtension()->getFileRelative(\DIRECTORY_SEPARATOR.'project'.\DIRECTORY_SEPARATOR.'file.txt')); } - /** - * @dataProvider getClassNameProvider - */ - public function testGettingClassAbbreviation($class, $abbr) + public function testClassAbbreviationIntegration() { - $this->assertEquals($this->getExtension()->abbrClass($class), $abbr); + $data = [ + 'fqcn' => 'F\Q\N\Foo', + 'xss' => '' ); $extension = new DumpExtension(new VarCloner(), $dumper); - $twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), [ + $twig = new Environment($this->createMock(LoaderInterface::class), [ 'debug' => true, 'cache' => false, 'optimizations' => 0, diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php index 4cbc55b46a66c..8ee7830974861 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubFilesystemLoader.php @@ -15,6 +15,9 @@ class StubFilesystemLoader extends FilesystemLoader { + /** + * @return string|null + */ protected function findTemplate($name, $throw = true) { // strip away bundle name diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php index 9abd707b40e0e..2c8c7db10d861 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/Fixtures/StubTranslator.php @@ -15,7 +15,7 @@ class StubTranslator implements TranslatorInterface { - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans($id, array $parameters = [], $domain = null, $locale = null): string { return '[trans]'.strtr($id, $parameters).'[/trans]'; } diff --git a/src/Symfony/Bridg 10000 e/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php index 384b9391cc4d6..9f0b0c070a2d9 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3HorizontalLayoutTest.php @@ -18,6 +18,7 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; class FormExtensionBootstrap3HorizontalLayoutTest extends AbstractBootstrap3HorizontalLayoutTest @@ -33,7 +34,7 @@ class FormExtensionBootstrap3HorizontalLayoutTest extends AbstractBootstrap3Hori */ private $renderer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -50,13 +51,13 @@ protected function setUp() 'bootstrap_3_horizontal_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -65,42 +66,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php index 2e75e3f7a852b..3535d4b6356e0 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap3LayoutTest.php @@ -18,6 +18,7 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; class FormExtensionBootstrap3LayoutTest extends AbstractBootstrap3LayoutTest @@ -29,7 +30,7 @@ class FormExtensionBootstrap3LayoutTest extends AbstractBootstrap3LayoutTest */ private $renderer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -46,7 +47,7 @@ protected function setUp() 'bootstrap_3_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } @@ -88,7 +89,7 @@ public function testMoneyWidgetInIso() 'bootstrap_3_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory @@ -101,12 +102,12 @@ public function testMoneyWidgetInIso()
HTML - , trim($this->renderWidget($view))); + , trim($this->renderWidget($view))); } protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -115,42 +116,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php index 243658764cc08..5c2e5afcfdf99 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4HorizontalLayoutTest.php @@ -18,6 +18,7 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; /** @@ -35,7 +36,7 @@ class FormExtensionBootstrap4HorizontalLayoutTest extends AbstractBootstrap4Hori private $renderer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -52,13 +53,13 @@ protected function setUp() 'bootstrap_4_horizontal_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -67,42 +68,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php index a0290a2049da6..5158c5a1e895c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionBootstrap4LayoutTest.php @@ -18,6 +18,7 @@ use Symfony\Bridge\Twig\Tests\Extension\Fixtures\StubTranslator; use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; /** @@ -33,7 +34,7 @@ class FormExtensionBootstrap4LayoutTest extends AbstractBootstrap4LayoutTest */ private $renderer; - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -50,7 +51,7 @@ protected function setUp() 'bootstrap_4_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } @@ -92,7 +93,7 @@ public function testMoneyWidgetInIso() 'bootstrap_4_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory @@ -105,12 +106,12 @@ public function testMoneyWidgetInIso()
HTML - , trim($this->renderWidget($view))); + , trim($this->renderWidget($view))); } protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -119,42 +120,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php index e40e57505a0a5..99477a617229c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionDivLayoutTest.php @@ -20,6 +20,7 @@ use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Tests\AbstractDivLayoutTest; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; class FormExtensionDivLayoutTest extends AbstractDivLayoutTest @@ -31,9 +32,7 @@ class FormExtensionDivLayoutTest extends AbstractDivLayoutTest */ private $renderer; - protected static $supportedFeatureSetVersion = 403; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -53,7 +52,7 @@ protected function setUp() 'form_div_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } @@ -181,7 +180,7 @@ public function testMoneyWidgetInIso() 'form_div_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); $view = $this->factory @@ -297,7 +296,7 @@ public function testHelpHtmlIsTrue() protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -306,42 +305,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php index 9570e03e523c7..967e25ec6ec45 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/FormExtensionTableLayoutTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Form\FormRenderer; use Symfony\Component\Form\FormView; use Symfony\Component\Form\Tests\AbstractTableLayoutTest; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Twig\Environment; class FormExtensionTableLayoutTest extends AbstractTableLayoutTest @@ -30,9 +31,7 @@ class FormExtensionTableLayoutTest extends AbstractTableLayoutTest */ private $renderer; - protected static $supportedFeatureSetVersion = 403; - - protected function setUp() + protected function setUp(): void { parent::setUp(); @@ -50,7 +49,7 @@ protected function setUp() 'form_table_layout.html.twig', 'custom_widgets.html.twig', ], $environment); - $this->renderer = new FormRenderer($rendererEngine, $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock()); + $this->renderer = new FormRenderer($rendererEngine, $this->createMock(CsrfTokenManagerInterface::class)); $this->registerTwigRuntimeLoader($environment, $this->renderer); } @@ -183,7 +182,7 @@ public function testHelpHtmlIsTrue() protected function renderForm(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form', $vars); + return $this->renderer->renderBlock($view, 'form', $vars); } protected function renderLabel(FormView $view, $label = null, array $vars = []) @@ -192,42 +191,42 @@ protected function renderLabel(FormView $view, $label = null, array $vars = []) $vars += ['label' => $label]; } - return (string) $this->renderer->searchAndRenderBlock($view, 'label', $vars); + return $this->renderer->searchAndRenderBlock($view, 'label', $vars); } protected function renderHelp(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'help'); + return $this->renderer->searchAndRenderBlock($view, 'help'); } protected function renderErrors(FormView $view) { - return (string) $this->renderer->searchAndRenderBlock($view, 'errors'); + return $this->renderer->searchAndRenderBlock($view, 'errors'); } protected function renderWidget(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'widget', $vars); + return $this->renderer->searchAndRenderBlock($view, 'widget', $vars); } protected function renderRow(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'row', $vars); + return $this->renderer->searchAndRenderBlock($view, 'row', $vars); } protected function renderRest(FormView $view, array $vars = []) { - return (string) $this->renderer->searchAndRenderBlock($view, 'rest', $vars); + return $this->renderer->searchAndRenderBlock($view, 'rest', $vars); } protected function renderStart(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_start', $vars); + return $this->renderer->renderBlock($view, 'form_start', $vars); } protected function renderEnd(FormView $view, array $vars = []) { - return (string) $this->renderer->renderBlock($view, 'form_end', $vars); + return $this->renderer->renderBlock($view, 'form_end', $vars); } protected function setTheme(FormView $view, array $themes, $useDefaultThemes = true) diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php index 396f433bfda63..cb6658e4bbd32 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpFoundationExtensionTest.php @@ -23,7 +23,7 @@ class HttpFoundationExtensionTest extends TestCase { /** - * @dataProvider getGenerateAbsoluteUrlData() + * @dataProvider getGenerateAbsoluteUrlData */ public function testGenerateAbsoluteUrl($expected, $path, $pathinfo) { @@ -62,7 +62,7 @@ public function getGenerateAbsoluteUrlData() */ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host, $scheme, $httpPort, $httpsPort, $expected) { - if (!class_exists('Symfony\Component\Routing\RequestContext')) { + if (!class_exists(RequestContext::class)) { $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); } @@ -77,7 +77,7 @@ public function testGenerateAbsoluteUrlWithRequestContext($path, $baseUrl, $host */ public function testGenerateAbsoluteUrlWithoutRequestAndRequestContext($path) { - if (!class_exists('Symfony\Component\Routing\RequestContext')) { + if (!class_exists(RequestContext::class)) { $this->markTestSkipped('The Routing component is needed to run tests that depend on its request context.'); } @@ -116,11 +116,11 @@ public function testGenerateAbsoluteUrlWithScriptFileName() } /** - * @dataProvider getGenerateRelativePathData() + * @dataProvider getGenerateRelativePathData */ public function testGenerateRelativePath($expected, $path, $pathinfo) { - if (!method_exists('Symfony\Component\HttpFoundation\Request', 'getRelativeUriForPath')) { + if (!method_exists(Request::class, 'getRelativeUriForPath')) { $this->markTestSkipped('Your version of Symfony HttpFoundation is too old.'); } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php index 22084ec1ae616..5fa1ef3bad62c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php @@ -15,18 +15,19 @@ use Symfony\Bridge\Twig\Extension\HttpKernelExtension; use Symfony\Bridge\Twig\Extension\HttpKernelRuntime; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; +use Symfony\Component\HttpKernel\Fragment\FragmentRendererInterface; use Twig\Environment; use Twig\Loader\ArrayLoader; +use Twig\RuntimeLoader\RuntimeLoaderInterface; class HttpKernelExtensionTest extends TestCase { - /** - * @expectedException \Twig\Error\RuntimeError - */ public function testFragmentWithError() { + $this->expectException(\Twig\Error\RuntimeError::class); $renderer = $this->getFragmentHandler($this->throwException(new \Exception('foo'))); $this->renderTemplate($renderer); @@ -43,32 +44,22 @@ public function testRenderFragment() public function testUnknownFragmentRenderer() { - $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') - ->disableOriginalConstructor() - ->getMock() - ; + $context = $this->createMock(RequestStack::class); $renderer = new FragmentHandler($context); - if (method_exists($this, 'expectException')) { - $this->expectException('InvalidArgumentException'); - $this->expectExceptionMessage('The "inline" renderer does not exist.'); - } else { - $this->setExpectedException('InvalidArgumentException', 'The "inline" renderer does not exist.'); - } + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The "inline" renderer does not exist.'); $renderer->render('/foo'); } protected function getFragmentHandler($return) { - $strategy = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\Fragment\\FragmentRendererInterface')->getMock(); + $strategy = $this->createMock(FragmentRendererInterface::class); $strategy->expects($this->once())->method('getName')->willReturn('inline'); $strategy->expects($this->once())->method('render')->will($return); - $context = $this->getMockBuilder('Symfony\\Component\\HttpFoundation\\RequestStack') - ->disableOriginalConstructor() - ->getMock() - ; + $context = $this->createMock(RequestStack::class); $context->expects($this->any())->method('getCurrentRequest')->willReturn(Request::create('/')); @@ -81,7 +72,7 @@ protected function renderTemplate(FragmentHandler $renderer, $template = '{{ ren $twig = new Environment($loader, ['debug' => true, 'cache' => false]); $twig->addExtension(new HttpKernelExtension()); - $loader = $this->getMockBuilder('Twig\RuntimeLoader\RuntimeLoaderInterface')->getMock(); + $loader = $this->createMock(RuntimeLoaderInterface::class); $loader->expects($this->any())->method('load')->willReturnMap([ ['Symfony\Bridge\Twig\Extension\HttpKernelRuntime', new HttpKernelRuntime($renderer)], ]); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php index 05f7ad741c7ea..5a995c8eeb76c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/RoutingExtensionTest.php @@ -13,7 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\RoutingExtension; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\FilterExpression; use Twig\Source; @@ -24,8 +26,8 @@ class RoutingExtensionTest extends TestCase */ public function testEscaping($template, $mustBeEscaped) { - $twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]); - $twig->addExtension(new RoutingExtension($this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock())); + $twig = new Environment($this->createMock(LoaderInterface::class), ['debug' => true, 'cache' => false, 'autoescape' => 'html', 'optimizations' => 0]); + $twig->addExtension(new RoutingExtension($this->createMock(UrlGeneratorInterface::class))); $nodes = $twig->parse($twig->tokenize(new Source($template, ''))); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/RuntimeLoaderProvider.php b/src/Symfony/Bridge/Twig/Tests/Extension/RuntimeLoaderProvider.php index 0bec8ec6f1aab..dea148192475a 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/RuntimeLoaderProvider.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/RuntimeLoaderProvider.php @@ -13,12 +13,13 @@ use Symfony\Component\Form\FormRenderer; use Twig\Environment; +use Twig\RuntimeLoader\RuntimeLoaderInterface; trait RuntimeLoaderProvider { protected function registerTwigRuntimeLoader(Environment $environment, FormRenderer $renderer) { - $loader = $this->getMockBuilder('Twig\RuntimeLoader\RuntimeLoaderInterface')->getMock(); + $loader = $this->createMock(RuntimeLoaderInterface::class); $loader->expects($this->any())->method('load')->will($this->returnValueMap([ ['Symfony\Component\Form\FormRenderer', $renderer], ])); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php index 1af65e4c19a7d..65f1bd69bff7c 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/StopwatchExtensionTest.php @@ -13,17 +13,16 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\StopwatchExtension; +use Symfony\Component\Stopwatch\Stopwatch; use Twig\Environment; use Twig\Error\RuntimeError; use Twig\Loader\ArrayLoader; class StopwatchExtensionTest extends TestCase { - /** - * @expectedException \Twig\Error\SyntaxError - */ public function testFailIfStoppingWrongEvent() { + $this->expectException(\Twig\Error\SyntaxError::class); $this->testTiming('{% stopwatch "foo" %}{% endstopwatch "bar" %}', []); } @@ -36,7 +35,7 @@ public function testTiming($template, $events) $twig->addExtension(new StopwatchExtension($this->getStopwatch($events))); try { - $nodes = $twig->render('template'); + $twig->render('template'); } catch (RuntimeError $e) { throw $e->getPrevious(); } @@ -57,20 +56,24 @@ public function getTimingTemplates() protected function getStopwatch($events = []) { $events = \is_array($events) ? $events : [$events]; - $stopwatch = $this->getMockBuilder('Symfony\Component\Stopwatch\Stopwatch')->getMock(); + $stopwatch = $this->createMock(Stopwatch::class); - $i = -1; + $expectedCalls = 0; + $expectedStartCalls = []; + $expectedStopCalls = []; foreach ($events as $eventName) { - $stopwatch->expects($this->at(++$i)) - ->method('start') - ->with($this->equalTo($eventName), 'template') - ; - $stopwatch->expects($this->at(++$i)) - ->method('stop') - ->with($this->equalTo($eventName)) - ; + ++$expectedCalls; + $expectedStartCalls[] = [$this->equalTo($eventName), 'template']; + $expectedStopCalls[] = [$this->equalTo($eventName)]; } + $startInvocationMocker = $stopwatch->expects($this->exactly($expectedCalls)) + ->method('start'); + \call_user_func_array([$startInvocationMocker, 'withConsecutive'], $expectedStartCalls); + $stopInvocationMocker = $stopwatch->expects($this->exactly($expectedCalls)) + ->method('stop'); + \call_user_func_array([$stopInvocationMocker, 'withConsecutive'], $expectedStopCalls); + return $stopwatch; } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php index de85603a5352b..a02fd2b29552d 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php @@ -15,8 +15,10 @@ use Symfony\Bridge\Twig\Extension\TranslationExtension; use Symfony\Component\Translation\Loader\ArrayLoader; use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; use Twig\Loader\ArrayLoader as TwigArrayLoader; +use Twig\TemplateWrapper; class TranslationExtensionTest extends TestCase { @@ -54,32 +56,28 @@ public function testTransChoice($template, $expected, array $variables = []) $this->testTrans($template, $expected, $variables); } - /** - * @expectedException \Twig\Error\SyntaxError - * @expectedExceptionMessage Unexpected token. Twig was looking for the "with", "from", or "into" keyword in "index" at line 3. - */ public function testTransUnknownKeyword() { - $output = $this->getTemplate("{% trans \n\nfoo %}{% endtrans %}")->render(); + $this->expectException(\Twig\Error\SyntaxError::class); + $this->expectExceptionMessage('Unexpected token. Twig was looking for the "with", "from", or "into" keyword in "index" at line 3.'); + $this->getTemplate("{% trans \n\nfoo %}{% endtrans %}")->render(); } - /** - * @expectedException \Twig\Error\SyntaxError - * @expectedExceptionMessage A message inside a trans tag must be a simple text in "index" at line 2. - */ public function testTransComplexBody() { - $output = $this->getTemplate("{% trans %}\n{{ 1 + 2 }}{% endtrans %}")->render(); + $this->expectException(\Twig\Error\SyntaxError::class); + $this->expectExceptionMessage('A message inside a trans tag must be a simple text in "index" at line 2.'); + $this->getTemplate("{% trans %}\n{{ 1 + 2 }}{% endtrans %}")->render(); } /** * @group legacy - * @expectedException \Twig\Error\SyntaxError - * @expectedExceptionMessage A message inside a transchoice tag must be a simple text in "index" at line 2. */ public function testTransChoiceComplexBody() { - $output = $this->getTemplate("{% transchoice count %}\n{{ 1 + 2 }}{% endtranschoice %}")->render(); + $this->expectException(\Twig\Error\SyntaxError::class); + $this->expectExceptionMessage('A message inside a transchoice tag must be a simple text in "index" at line 2.'); + $this->getTemplate("{% transchoice count %}\n{{ 1 + 2 }}{% endtranschoice %}")->render(); } public function getTransTests() @@ -273,7 +271,7 @@ public function testDefaultTranslationDomainWithNamedArguments() $this->assertEquals('foo (custom)foo (foo)foo (custom)foo (custom)foo (fr)foo (custom)foo (fr)', trim($template->render([]))); } - protected function getTemplate($template, $translator = null) + private function getTemplate($template, TranslatorInterface $translator = null): TemplateWrapper { if (null === $translator) { $translator = new Translator('en'); @@ -287,6 +285,6 @@ protected function getTemplate($template, $translator = null) $twig = new Environment($loader, ['debug' => true, 'cache' => false]); $twig->addExtension(new TranslationExtension($translator)); - return $twig->loadTemplate('index'); + return $twig->load('index'); } } diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php index f49eea396d0d8..1739c1ee91833 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WebLinkExtensionTest.php @@ -11,11 +11,11 @@ namespace Symfony\Bridge\Twig\Tests\Extension; -use Fig\Link\Link; use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\Extension\WebLinkExtension; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\WebLink\Link; /** * @author Kévin Dunglas @@ -32,7 +32,7 @@ class WebLinkExtensionTest extends TestCase */ private $extension; - protected function setUp() + protected function setUp(): void { $this->request = new Request(); diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php index 3e948bae3f50e..57a09b0a7e918 100644 --- a/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Extension/WorkflowExtensionTest.php @@ -28,7 +28,7 @@ class WorkflowExtensionTest extends TestCase private $extension; private $t1; - protected function setUp() + protected function setUp(): void { if (!class_exists(Workflow::class)) { $this->markTestSkipped('The Workflow component is needed to run tests for this extension.'); diff --git a/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php new file mode 100644 index 0000000000000..b22e99e66d22b --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Fixtures/TokenInterface.php @@ -0,0 +1,11 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Twig\Tests\Mime; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Mime\BodyRenderer; +use Symfony\Bridge\Twig\Mime\TemplatedEmail; +use Symfony\Component\Mime\Exception\InvalidArgumentException; +use Symfony\Component\Mime\Part\Multipart\AlternativePart; +use Twig\Environment; +use Twig\Loader\ArrayLoader; + +class BodyRendererTest extends TestCase +{ + public function testRenderTextOnly() + { + $email = $this->prepareEmail('Text', null); + $this->assertEquals('Text', $email->getBody()->bodyToString()); + } + + public function testRenderHtmlOnly() + { + $html = 'headHTML'; + $email = $this->prepareEmail(null, $html); + $body = $email->getBody(); + $this->assertInstanceOf(AlternativePart::class, $body); + $this->assertEquals('HTML', $body->getParts()[0]->bodyToString()); + $this->assertEquals(str_replace('=', '=3D', $html), $body->getParts()[1]->bodyToString()); + } + + public function testRenderMultiLineHtmlOnly() + { + $html = << + + +HTML +HTML; + $email = $this->prepareEmail(null, $html); + $body = $email->getBody(); + $this->assertInstanceOf(AlternativePart::class, $body); + $this->assertEquals('HTML', str_replace(["\r", "\n"], '', $body->getParts()[0]->bodyToString())); + $this->assertEquals(str_replace(['=', "\n"], ['=3D', "\r\n"], $html), $body->getParts()[1]->bodyToString()); + } + + public function testRenderHtmlOnlyWithTextSet() + { + $email = $this->prepareEmail(null, 'HTML'); + $email->text('Text'); + $body = $email->getBody(); + $this->assertInstanceOf(AlternativePart::class, $body); + $this->assertEquals('Text', $body->getParts()[0]->bodyToString()); + $this->assertEquals('HTML', $body->getParts()[1]->bodyToString()); + } + + public function testRenderTextAndHtml() + { + $email = $this->prepareEmail('Text', 'HTML'); + $body = $email->getBody(); + $this->assertInstanceOf(AlternativePart::class, $body); + $this->assertEquals('Text', $body->getParts()[0]->bodyToString()); + $this->assertEquals('HTML', $body->getParts()[1]->bodyToString()); + } + + public function testRenderWithContextReservedEmailEntry() + { + $this->expectException(InvalidArgumentException::class); + $this->prepareEmail('Text', '', ['email' => 'reserved!']); + } + + public function testRenderedOnce() + { + $twig = new Environment(new ArrayLoader([ + 'text' => 'Text', + ])); + $renderer = new BodyRenderer($twig); + $email = (new TemplatedEmail()) + ->to('fabien@symfony.com') + ->from('helene@symfony.com') + ; + $email->textTemplate('text'); + + $renderer->render($email); + $this->assertEquals('Text', $email->getTextBody()); + + $email->text('reset'); + + $renderer->render($email); + $this->assertEquals('reset', $email->getTextBody()); + } + + public function testRenderedOnceUnserializableContext() + { + $twig = new Environment(new ArrayLoader([ + 'text' => 'Text', + ])); + $renderer = new BodyRenderer($twig); + $email = (new TemplatedEmail()) + ->to('fabien@symfony.com') + ->from('helene@symfony.com') + ; + $email->textTemplate('text'); + $email->context([ + 'foo' => static function () { + return 'bar'; + }, + ]); + + $renderer->render($email); + $this->assertEquals('Text', $email->getTextBody()); + } + + private function prepareEmail(?string $text, ?string $html, array $context = []): TemplatedEmail + { + $twig = new Environment(new ArrayLoader([ + 'text' => $text, + 'html' => $html, + 'document.txt' => 'Some text document...', + 'image.jpg' => 'Some image data', + ])); + $renderer = new BodyRenderer($twig); + $email = (new TemplatedEmail()) + ->to('fabien@symfony.com') + ->from('helene@symfony.com') + ->context($context) + ; + if (null !== $text) { + $email->textTemplate('text'); + } + if (null !== $html) { + $email->htmlTemplate('html'); + } + $renderer->render($email); + + return $email; + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/NotificationEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/NotificationEmailTest.php new file mode 100644 index 0000000000000..0b55ca01d6b3e --- /dev/null +++ b/src/Symfony/Bridge/Twig/Tests/Mime/NotificationEmailTest.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\Bridge\Twig\Tests\Mime; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\Mime\NotificationEmail; + +class NotificationEmailTest extends TestCase +{ + public function test() + { + $email = (new NotificationEmail()) + ->markdown('Foo') + ->exception(new \Exception()) + ->importance(NotificationEmail::IMPORTANCE_HIGH) + ->action('Bar', 'http://example.com/') + ->context(['a' => 'b']) + ; + + $this->assertEquals([ + 'importance' => NotificationEmail::IMPORTANCE_HIGH, + 'content' => 'Foo', + 'exception' => true, + 'action_text' => 'Bar', + 'action_url' => 'http://example.com/', + 'markdown' => true, + 'raw' => false, + 'a' => 'b', + ], $email->getContext()); + } + + public function testSerialize() + { + $email = unserialize(serialize((new NotificationEmail()) + ->content('Foo', true) + ->exception(new \Exception()) + ->importance(NotificationEmail::IMPORTANCE_HIGH) + ->action('Bar', 'http://example.com/') + ->context(['a' => 'b']) + ->theme('example') + )); + $this->assertEquals([ + 'importance' => NotificationEmail::IMPORTANCE_HIGH, + 'content' => 'Foo', + 'exception' => true, + 'action_text' => 'Bar', + 'action_url' => 'http://example.com/', + 'markdown' => false, + 'raw' => true, + 'a' => 'b', + ], $email->getContext()); + + $this->assertSame('@email/example/notification/body.html.twig', $email->getHtmlTemplate()); + } + + public function testTheme() + { + $email = (new NotificationEmail())->theme('mine'); + $this->assertSame('@email/mine/notification/body.html.twig', $email->getHtmlTemplate()); + $this->assertSame('@email/mine/notification/body.txt.twig', $email->getTextTemplate()); + } + + public function testSubject() + { + $email = (new NotificationEmail())->from('me@example.com')->subject('Foo'); + $headers = $email->getPreparedHeaders(); + $this->assertSame('[LOW] Foo', $headers->get('Subject')->getValue()); + } +} diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/RendererTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/RendererTest.php deleted file mode 100644 index 3c40e6d7ee049..0000000000000 --- a/src/Symfony/Bridge/Twig/Tests/Mime/RendererTest.php +++ /dev/null @@ -1,77 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Symfony\Bridge\Twig\Tests\Mime; - -use PHPUnit\Framework\TestCase; -use Symfony\Bridge\Twig\Mime\BodyRenderer; -use Symfony\Bridge\Twig\Mime\TemplatedEmail; -use Symfony\Component\Mime\Part\Multipart\AlternativePart; -use Twig\Environment; -use Twig\Loader\ArrayLoader; - -class RendererTest extends TestCase -{ - public function testRenderTextOnly(): void - { - $email = $this->prepareEmail('Text', null); - $this->assertEquals('Text', $email->getBody()->bodyToString()); - } - - public function testRenderHtmlOnly(): void - { - $email = $this->prepareEmail(null, 'HTML'); - $body = $email->getBody(); - $this->assertInstanceOf(AlternativePart::class, $body); - $this->assertEquals('HTML', $body->getParts()[0]->bodyToString()); - $this->assertEquals('HTML', $body->getParts()[1]->bodyToString()); - } - - public function testRenderHtmlOnlyWithTextSet(): void - { - $email = $this->prepareEmail(null, 'HTML'); - $email->text('Text'); - $body = $email->getBody(); - $this->assertInstanceOf(AlternativePart::class, $body); - $this->assertEquals('Text', $body->getParts()[0]->bodyToString()); - $this->assertEquals('HTML', $body->getParts()[1]->bodyToString()); - } - - public function testRenderTextAndHtml(): void - { - $email = $this->prepareEmail('Text', 'HTML'); - $body = $email->getBody(); - $this->assertInstanceOf(AlternativePart::class, $body); - $this->assertEquals('Text', $body->getParts()[0]->bodyToString()); - $this->assertEquals('HTML', $body->getParts()[1]->bodyToString()); - } - - private function prepareEmail(?string $text, ?string $html): TemplatedEmail - { - $twig = new Environment(new ArrayLoader([ - 'text' => $text, - 'html' => $html, - 'document.txt' => 'Some text document...', - 'image.jpg' => 'Some image data', - ])); - $renderer = new BodyRenderer($twig); - $email = (new TemplatedEmail())->to('fabien@symfony.com')->from('helene@symfony.com'); - if (null !== $text) { - $email->textTemplate('text'); - } - if (null !== $html) { - $email->htmlTemplate('html'); - } - $renderer->render($email); - - return $email; - } -} diff --git a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php index 999ca4d078d58..27ac35e039f4b 100644 --- a/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Mime/TemplatedEmailTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bridge\Twig\Tests\Mime; use PHPUnit\Framework\TestCase; diff --git a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php index 06c75fd0ecad7..f655a04ae3bd6 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/DumpNodeTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Node\DumpNode; use Twig\Compiler; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\NameExpression; use Twig\Node\Node; @@ -24,7 +25,7 @@ public function testNoVar() { $node = new DumpNode('bar', null, 7); - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); + $env = new Environment($this->createMock(LoaderInterface::class)); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -48,7 +49,7 @@ public function testIndented() { $node = new DumpNode('bar', null, 7); - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); + $env = new Environment($this->createMock(LoaderInterface::class)); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -75,7 +76,7 @@ public function testOneVar() ]); $node = new DumpNode('bar', $vars, 7); - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); + $env = new Environment($this->createMock(LoaderInterface::class)); $compiler = new Compiler($env); $expected = <<<'EOTXT' @@ -99,7 +100,7 @@ public function testMultiVars() ]); $node = new DumpNode('bar', $vars, 7); - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); + $env = new Environment($this->createMock(LoaderInterface::class)); $compiler = new Compiler($env); $expected = <<<'EOTXT' diff --git a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php index 848685ee5fbf3..ab45b83fecd72 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/FormThemeTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Form\FormRendererEngineInterface; use Twig\Compiler; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; @@ -54,8 +55,8 @@ public function testCompile() $node = new FormThemeNode($form, $resources, 0); - $environment = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); - $formRenderer = new FormRenderer($this->getMockBuilder(FormRendererEngineInterface::class)->getMock()); + $environment = new Environment($this->createMock(LoaderInterface::class)); + $formRenderer = new FormRenderer($this->createMock(FormRendererEngineInterface::class)); $this->registerTwigRuntimeLoader($environment, $formRenderer); $compiler = new Compiler($environment); @@ -63,7 +64,7 @@ public function testCompile() sprintf( '$this->env->getRuntime("Symfony\\\\Component\\\\Form\\\\FormRenderer")->setTheme(%s, [0 => "tpl1", 1 => "tpl2"], true);', $this->getVariableGetter('form') - ), + ), trim($compiler->compile($node)->getSource()) ); @@ -73,7 +74,7 @@ public function testCompile() sprintf( '$this->env->getRuntime("Symfony\\\\Component\\\\Form\\\\FormRenderer")->setTheme(%s, [0 => "tpl1", 1 => "tpl2"], false);', $this->getVariableGetter('form') - ), + ), trim($compiler->compile($node)->getSource()) ); @@ -85,7 +86,7 @@ public function testCompile() sprintf( '$this->env->getRuntime("Symfony\\\\Component\\\\Form\\\\FormRenderer")->setTheme(%s, "tpl1", true);', $this->getVariableGetter('form') - ), + ), trim($compiler->compile($node)->getSource()) ); @@ -95,7 +96,7 @@ public function testCompile() sprintf( '$this->env->getRuntime("Symfony\\\\Component\\\\Form\\\\FormRenderer")->setTheme(%s, "tpl1", false);', $this->getVariableGetter('form') - ), + ), trim($compiler->compile($node)->getSource()) ); } diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php index fab7d0974c5e6..42cb1762b050d 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Node\SearchAndRenderBlockNode; use Twig\Compiler; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConditionalExpression; use Twig\Node\Expression\ConstantExpression; @@ -31,13 +32,13 @@ public function testCompileWidget() $node = new SearchAndRenderBlockNode('form_widget', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); $this->assertEquals( sprintf( '$this->env->getRuntime(\'Symfony\Component\Form\FormRenderer\')->searchAndRenderBlock(%s, \'widget\')', $this->getVariableGetter('form') - ), + ), trim($compiler->compile($node)->getSource()) ); } @@ -54,7 +55,7 @@ public function testCompileWidgetWithVariables() $node = new SearchAndRenderBlockNode('form_widget', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); $this->assertEquals( sprintf( @@ -74,7 +75,7 @@ public function testCompileLabelWithLabel() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); $this->assertEquals( sprintf( @@ -94,7 +95,7 @@ public function testCompileLabelWithNullLabel() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -116,7 +117,7 @@ public function testCompileLabelWithEmptyStringLabel() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -137,7 +138,7 @@ public function testCompileLabelWithDefaultLabel() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); $this->assertEquals( sprintf( @@ -161,7 +162,7 @@ public function testCompileLabelWithAttributes() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -190,7 +191,7 @@ public function testCompileLabelWithLabelAndAttributes() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); $this->assertEquals( sprintf( @@ -218,7 +219,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNull() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. @@ -255,7 +256,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes() $node = new SearchAndRenderBlockNode('form_label', $arguments, 0); - $compiler = new Compiler(new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock())); + $compiler = new Compiler(new Environment($this->createMock(LoaderInterface::class))); // "label" => null must not be included in the output! // Otherwise the default label is overwritten with null. diff --git a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php index f974f08580360..c6d3064676937 100644 --- a/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Node/TransNodeTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Node\TransNode; use Twig\Compiler; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\NameExpression; use Twig\Node\TextNode; @@ -29,7 +30,7 @@ public function testCompileStrict() $vars = new NameExpression('foo', 0); $node = new TransNode($body, null, null, $vars); - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['strict_variables' => true]); + $env = new Environment($this->createMock(LoaderInterface::class), ['strict_variables' => true]); $compiler = new Compiler($env); $this->assertEquals( @@ -37,8 +38,8 @@ public function testCompileStrict() 'echo $this->env->getExtension(\'Symfony\Bridge\Twig\Extension\TranslationExtension\')->trans("trans %%var%%", array_merge(["%%var%%" => %s], %s), "messages");', $this->getVariableGetterWithoutStrictCheck('var'), $this->getVariableGetterWithStrictCheck('foo') - ), - trim($compiler->compile($node)->getSource()) + ), + trim($compiler->compile($node)->getSource()) ); } diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php index 5abfe1e92a6ca..1c3d42d1a3192 100644 --- a/src/Symfony/Bridge/Twi 10000 g/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationDefaultDomainNodeVisitorTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\NodeVisitor\TranslationDefaultDomainNodeVisitor; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Node; @@ -26,7 +27,7 @@ class TranslationDefaultDomainNodeVisitorTest extends TestCase /** @dataProvider getDefaultDomainAssignmentTestData */ public function testDefaultDomainAssignment(Node $node) { - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationDefaultDomainNodeVisitor(); // visit trans_default_domain tag @@ -52,7 +53,7 @@ public function testDefaultDomainAssignment(Node $node) /** @dataProvider getDefaultDomainAssignmentTestData */ public function testNewModuleWithoutDefaultDomainTag(Node $node) { - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationDefaultDomainNodeVisitor(); // visit trans_default_domain tag diff --git a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php index 11d16e4cd6744..8d8b77c3acf53 100644 --- a/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/NodeVisitor/TranslationNodeVisitorTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\NodeVisitor\TranslationNodeVisitor; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\FilterExpression; @@ -25,7 +26,7 @@ class TranslationNodeVisitorTest extends TestCase /** @dataProvider getMessagesExtractionTestData */ public function testMessagesExtraction(Node $node, array $expectedMessages) { - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $visitor = new TranslationNodeVisitor(); $visitor->enable(); $visitor->enterNode($node, $env); diff --git a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php index 71180156ba2b1..d404060091f83 100644 --- a/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php +++ b/src/Symfony/Bridge/Twig/Tests/TokenParser/FormThemeTokenParserTest.php @@ -15,6 +15,7 @@ use Symfony\Bridge\Twig\Node\FormThemeNode; use Symfony\Bridge\Twig\TokenParser\FormThemeTokenParser; use Twig\Environment; +use Twig\Loader\LoaderInterface; use Twig\Node\Expression\ArrayExpression; use Twig\Node\Expression\ConstantExpression; use Twig\Node\Expression\NameExpression; @@ -28,15 +29,13 @@ class FormThemeTokenParserTest extends TestCase */ public function testCompile($source, $expected) { - $env = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); + $env = new Environment($this->createMock(LoaderInterface::class), ['cache' => false, 'autoescape' => false, 'optimizations' => 0]); $env->addTokenParser(new FormThemeTokenParser()); $source = new Source($source, ''); $stream = $env->tokenize($source); $parser = new Parser($env); - if (method_exists($expected, 'setSourceContext')) { - $expected->setSourceContext($source); - } + $expected->setSourceContext($source); $this->assertEquals($expected, $parser->parse($stream)->getNode('body')->getNode(0)); } diff --git a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php index 1f5c1955c7eb4..5dfc6ea450e0e 100644 --- a/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php +++ b/src/Symfony/Bridge/Twig/Tests/Translation/TwigExtractorTest.php @@ -17,8 +17,8 @@ use Symfony\Component\Translation\MessageCatalogue; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Environment; -use Twig\Error\Error; use Twig\Loader\ArrayLoader; +use Twig\Loader\LoaderInterface; class TwigExtractorTest extends TestCase { @@ -27,14 +27,14 @@ class TwigExtractorTest extends TestCase */ public function testExtract($template, $messages) { - $loader = $this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(LoaderInterface::class); $twig = new Environment($loader, [ 'strict_variables' => true, 'debug' => true, 'cache' => false, 'autoescape' => false, ]); - $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); + $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); $extractor = new TwigExtractor($twig); $extractor->setPrefix('prefix'); @@ -99,39 +99,25 @@ public function getLegacyExtractData() } /** - * @expectedException \Twig\Error\Error * @dataProvider resourcesWithSyntaxErrorsProvider */ - public function testExtractSyntaxError($resources) + public function testExtractSyntaxError($resources, array $messages) { - $twig = new Environment($this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock()); - $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); + $twig = new Environment($this->createMock(LoaderInterface::class)); + $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); $extractor = new TwigExtractor($twig); - - try { - $extractor->extract($resources, new MessageCatalogue('en')); - } catch (Error $e) { - if (method_exists($e, 'getSourceContext')) { - $this->assertSame(\dirname(__DIR__).strtr('/Fixtures/extractor/syntax_error.twig', '/', \DIRECTORY_SEPARATOR), $e->getFile()); - $this->assertSame(1, $e->getLine()); - $this->assertSame('Unclosed "block".', $e->getMessage()); - } else { - $this->expectExceptionMessageRegExp('/Unclosed "block" in ".*extractor(\\/|\\\\)syntax_error\\.twig" at line 1/'); - } - throw $e; - } + $catalogue = new MessageCatalogue('en'); + $extractor->extract($resources, $catalogue); + $this->assertSame($messages, $catalogue->all()); } - /** - * @return array - */ - public function resourcesWithSyntaxErrorsProvider() + public function resourcesWithSyntaxErrorsProvider(): array { return [ - [__DIR__.'/../Fixtures'], - [__DIR__.'/../Fixtures/extractor/syntax_error.twig'], - [new \SplFileInfo(__DIR__.'/../Fixtures/extractor/syntax_error.twig')], + [__DIR__.'/../Fixtures', ['messages' => ['Hi!' => 'Hi!']]], + [__DIR__.'/../Fixtures/extractor/syntax_error.twig', []], + [new \SplFileInfo(__DIR__.'/../Fixtures/extractor/syntax_error.twig'), []], ]; } @@ -147,7 +133,7 @@ public function testExtractWithFiles($resource) 'cache' => false, 'autoescape' => false, ]); - $twig->addExtension(new TranslationExtension($this->getMockBuilder(TranslatorInterface::class)->getMock())); + $twig->addExtension(new TranslationExtension($this->createMock(TranslatorInterface::class))); $extractor = new TwigExtractor($twig); $catalogue = new MessageCatalogue('en'); @@ -157,10 +143,7 @@ public function testExtractWithFiles($resource) $this->assertEquals('Hi!', $catalogue->get('Hi!', 'messages')); } - /** - * @return array - */ - public function resourceProvider() + public function resourceProvider(): array { $directory = __DIR__.'/../Fixtures/extractor/'; diff --git a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php index 89eba52167945..6f504cb399f36 100644 --- a/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php +++ b/src/Symfony/Bridge/Twig/Tests/TwigEngineTest.php @@ -13,9 +13,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\Twig\TwigEngine; +use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Templating\TemplateReference; use Twig\Environment; +use Twig\Error\SyntaxError; use Twig\Loader\ArrayLoader; +use Twig\Template; /** * @group legacy @@ -26,7 +29,7 @@ public function testExistsWithTemplateInstances() { $engine = $this->getTwig(); - $this->assertTrue($engine->exists($this->getMockForAbstractClass('Twig\Template', [], '', false))); + $this->assertTrue($engine->exists($this->getMockForAbstractClass(Template::class, [], '', false))); } public function testExistsWithNonExistentTemplates() @@ -61,11 +64,9 @@ public function testRender() $this->assertSame('foo', $engine->render(new TemplateReference('index'))); } - /** - * @expectedException \Twig\Error\SyntaxError - */ public function testRenderWithError() { + $this->expectException(SyntaxError::class); $engine = $this->getTwig(); $engine->render(new TemplateReference('error')); @@ -77,7 +78,7 @@ protected function getTwig() 'index' => 'foo', 'error' => '{{ foo }', ])); - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); + $parser = $this->createMock(TemplateNameParserInterface::class); return new TwigEngine($twig, $parser); } diff --git a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php index a4d7d6f690078..c13297f23258d 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/DumpTokenParser.php @@ -12,6 +12,7 @@ namespace Symfony\Bridge\Twig\TokenParser; use Symfony\Bridge\Twig\Node\DumpNode; +use Twig\Node\Node; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -25,11 +26,15 @@ * {% dump foo, bar %} * * @author Julien Galenski + * + * @final since Symfony 4.4 */ class DumpTokenParser extends AbstractTokenParser { /** * {@inheritdoc} + * + * @return Node */ public function parse(Token $token) { @@ -44,6 +49,8 @@ public function parse(Token $token) /** * {@inheritdoc} + * + * @return string */ public function getTag() { diff --git a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php index ffef9e9859277..58fe3dd6be261 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/FormThemeTokenParser.php @@ -21,6 +21,8 @@ * Token Parser for the 'form_theme' tag. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class FormThemeTokenParser extends AbstractTokenParser { diff --git a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php index 54dcf6d391b0f..e1ac39cf8f390 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/StopwatchTokenParser.php @@ -13,6 +13,7 @@ use Symfony\Bridge\Twig\Node\StopwatchNode; use Twig\Node\Expression\AssignNameExpression; +use Twig\Node\Node; use Twig\Token; use Twig\TokenParser\AbstractTokenParser; @@ -20,6 +21,8 @@ * Token Parser for the stopwatch tag. * * @author Wouter J + * + * @final since Symfony 4.4 */ class StopwatchTokenParser extends AbstractTokenParser { @@ -30,6 +33,9 @@ public function __construct(bool $stopwatchIsAvailable) $this->stopwatchIsAvailable = $stopwatchIsAvailable; } + /** + * @return Node + */ public function parse(Token $token) { $lineno = $token->getLine(); @@ -56,6 +62,9 @@ public function decideStopwatchEnd(Token $token) return $token->test('endstopwatch'); } + /** + * @return string + */ public function getTag() { return 'stopwatch'; diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php index 08b44b27b80b7..c218091c70ff3 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransChoiceTokenParser.php @@ -18,6 +18,7 @@ use Twig\Node\Node; use Twig\Node\TextNode; use Twig\Token; +use Twig\TokenParser\AbstractTokenParser; /** * Token Parser for the 'transchoice' tag. @@ -25,22 +26,22 @@ * @author Fabien Potencier * * @deprecated since Symfony 4.2, use the "trans" tag with a "%count%" parameter instead + * + * @final since Symfony 4.4 */ -class TransChoiceTokenParser extends TransTokenParser +class TransChoiceTokenParser extends AbstractTokenParser { /** - * Parses a token and returns a node. + * {@inheritdoc} * * @return Node - * - * @throws SyntaxError */ public function parse(Token $token) { $lineno = $token->getLine(); $stream = $this->parser->getStream(); - @trigger_error(sprintf('The "transchoice" tag is deprecated since Symfony 4.2, use the "trans" one instead with a "%%count%%" parameter in %s line %d.', $stream->getSourceContext()->getName(), $lineno), E_USER_DEPRECATED); + @trigger_error(sprintf('The "transchoice" tag is deprecated since Symfony 4.2, use the "trans" one instead with a "%%count%%" parameter in %s line %d.', $stream->getSourceContext()->getName(), $lineno), \E_USER_DEPRECATED); $vars = new ArrayExpression([], $lineno); @@ -86,9 +87,9 @@ public function decideTransChoiceFork($token) } /** - * Gets the tag name associated with this token parser. + * {@inheritdoc} * - * @return string The tag name + * @return string */ public function getTag() { diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php index ee546e05f0125..e6b16680f7b7c 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransDefaultDomainTokenParser.php @@ -20,11 +20,13 @@ * Token Parser for the 'trans_default_domain' tag. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TransDefaultDomainTokenParser extends AbstractTokenParser { /** - * Parses a token and returns a node. + * {@inheritdoc} * * @return Node */ @@ -38,9 +40,9 @@ public function parse(Token $token) } /** - * Gets the tag name associated with this token parser. + * {@inheritdoc} * - * @return string The tag name + * @return string */ public function getTag() { diff --git a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php index 023e3dbf43434..e72240b6e8119 100644 --- a/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php +++ b/src/Symfony/Bridge/Twig/TokenParser/TransTokenParser.php @@ -24,15 +24,15 @@ * Token Parser for the 'trans' tag. * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class TransTokenParser extends AbstractTokenParser { /** - * Parses a token and returns a node. + * {@inheritdoc} * * @return Node - * - * @throws SyntaxError */ public function parse(Token $token) { @@ -90,9 +90,9 @@ public function decideTransFork($token) } /** - * Gets the tag name associated with this token parser. + * {@inheritdoc} * - * @return string The tag name + * @return string */ public function getTag() { diff --git a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php index a921582dbabdb..3d01ca57c7158 100644 --- a/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php +++ b/src/Symfony/Bridge/Twig/Translation/TwigExtractor.php @@ -12,7 +12,6 @@ namespace Symfony\Bridge\Twig\Translation; use Symfony\Component\Finder\Finder; -use Symfony\Component\Finder\SplFileInfo; use Symfony\Component\Translation\Extractor\AbstractFileExtractor; use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; @@ -58,17 +57,7 @@ public function extract($resource, MessageCatalogue $catalogue) try { $this->extractTemplate(file_get_contents($file->getPathname()), $catalogue); } catch (Error $e) { - if ($file instanceof \SplFileInfo) { - $path = $file->getRealPath() ?: $file->getPathname(); - $name = $file instanceof SplFileInfo ? $file->getRelativePathname() : $path; - if (method_exists($e, 'setSourceContext')) { - $e->setSourceContext(new Source('', $name, $path)); - } else { - $e->setTemplateName($name); - } - } - - throw $e; + // ignore errors, these should be fixed by using the linter } } } @@ -102,13 +91,11 @@ protected function extractTemplate($template, MessageCatalogue $catalogue) */ protected function canBeExtracted($file) { - return $this->isFile($file) && 'twig' === pathinfo($file, PATHINFO_EXTENSION); + return $this->isFile($file) && 'twig' === pathinfo($file, \PATHINFO_EXTENSION); } /** - * @param string|array $directory - * - * @return array + * {@inheritdoc} */ protected function extractFromDirectory($directory) { diff --git a/src/Symfony/Bridge/Twig/TwigEngine.php b/src/Symfony/Bridge/Twig/TwigEngine.php index 266d824bb4e6f..c31f0a470557a 100644 --- a/src/Symfony/Bridge/Twig/TwigEngine.php +++ b/src/Symfony/Bridge/Twig/TwigEngine.php @@ -11,7 +11,7 @@ namespace Symfony\Bridge\Twig; -@trigger_error('The '.TwigEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use \Twig\Environment instead.', E_USER_DEPRECATED); +@trigger_error('The '.TwigEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use \Twig\Environment instead.', \E_USER_DEPRECATED); use Symfony\Component\Templating\EngineInterface; use Symfony\Component\Templating\StreamingEngineInterface; @@ -21,6 +21,7 @@ use Twig\Error\Error; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; use Twig\Template; /** @@ -78,19 +79,24 @@ public function exists($name) $loader = $this->environment->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists((string) $name); - } + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + // cast possible TemplateReferenceInterface to string because the + // EngineInterface supports them but LoaderInterface does not + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext((string) $name); + } else { + $loader->getSource((string) $name); + } + + return true; + } catch (LoaderError $e) { + } - try { - // cast possible TemplateReferenceInterface to string because the - // EngineInterface supports them but LoaderInterface does not - $loader->getSourceContext((string) $name)->getCode(); - } catch (LoaderError $e) { return false; } - return true; + return $loader->exists((string) $name); } /** @@ -126,7 +132,7 @@ protected function load($name) } try { - return $this->environment->loadTemplate((string) $name); + return $this->environment->load((string) $name)->unwrap(); } catch (LoaderError $e) { throw new \InvalidArgumentException($e->getMessage(), $e->getCode(), $e); } diff --git a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php index 8c6253ef2b222..43aff010e3c48 100644 --- a/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php +++ b/src/Symfony/Bridge/Twig/UndefinedCallableHandler.php @@ -11,6 +11,7 @@ namespace Symfony\Bridge\Twig; +use Composer\InstalledVersions; use Symfony\Bundle\FullStack; use Twig\Error\SyntaxError; @@ -19,7 +20,7 @@ */ class UndefinedCallableHandler { - private static $filterComponents = [ + private const FILTER_COMPONENTS = [ 'humanize' => 'form', 'trans' => 'translation', 'transchoice' => 'translation', @@ -27,7 +28,7 @@ class UndefinedCallableHandler 'yaml_dump' => 'yaml', ]; - private static $functionComponents = [ + private const FUNCTION_COMPONENTS = [ 'asset' => 'asset', 'asset_version' => 'asset', 'dump' => 'debug-bundle', @@ -57,7 +58,7 @@ class UndefinedCallableHandler 'workflow_marked_places' => 'workflow', ]; - private static $fullStackEnable = [ + private const FULL_STACK_ENABLE = [ 'form' => 'enable "framework.form"', 'security-core' => 'add the "SecurityBundle"', 'security-http' => 'add the "SecurityBundle"', @@ -65,30 +66,40 @@ class UndefinedCallableHandler 'workflow' => 'enable "framework.workflows"', ]; - public static function onUndefinedFilter($name) + public static function onUndefinedFilter(string $name): bool { - if (!isset(self::$filterComponents[$name])) { + if (!isset(self::FILTER_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'filter', self::$filterComponents[$name]); + self::onUndefined($name, 'filter', self::FILTER_COMPONENTS[$name]); + + return true; } - public static function onUndefinedFunction($name) + public static function onUndefinedFunction(string $name): bool { - if (!isset(self::$functionComponents[$name])) { + if (!isset(self::FUNCTION_COMPONENTS[$name])) { return false; } - self::onUndefined($name, 'function', self::$functionComponents[$name]); + self::onUndefined($name, 'function', self::FUNCTION_COMPONENTS[$name]); + + return true; } - private static function onUndefined($name, $type, $component) + private static function onUndefined(string $name, string $type, string $component) { - if (class_exists(FullStack::class) && isset(self::$fullStackEnable[$component])) { - throw new SyntaxError(sprintf('Did you forget to %s? Unknown %s "%s".', self::$fullStackEnable[$component], $type, $name)); + if (class_exists(FullStack::class) && isset(self::FULL_STACK_ENABLE[$component])) { + throw new SyntaxError(sprintf('Did you forget to %s? Unknown %s "%s".', self::FULL_STACK_ENABLE[$component], $type, $name)); + } + + $missingPackage = 'symfony/'.$component; + + if (class_exists(InstalledVersions::class) && InstalledVersions::isInstalled($missingPackage)) { + $missingPackage = 'symfony/twig-bundle'; } - throw new SyntaxError(sprintf('Did you forget to run "composer require symfony/%s"? Unknown %s "%s".', $component, $type, $name)); + throw new SyntaxError(sprintf('Did you forget to run "composer require %s"? Unknown %s "%s".', $missingPackage, $type, $name)); } } diff --git a/src/Symfony/Bridge/Twig/composer.json b/src/Symfony/Bridge/Twig/composer.json index c4e6ab29d4ee1..14c737127e13b 100644 --- a/src/Symfony/Bridge/Twig/composer.json +++ b/src/Symfony/Bridge/Twig/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/twig-bridge", "type": "symfony-bridge", - "description": "Symfony Twig Bridge", + "description": "Provides integration for Twig with various Symfony components", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,18 +16,21 @@ } ], "require": { - "php": "^7.1.3", - "symfony/translation-contracts": "^1.1", - "twig/twig": "^1.41|^2.10" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16", + "symfony/translation-contracts": "^1.1|^2", + "twig/twig": "^1.43|^2.13|^3.0.4" }, "require-dev": { - "egulias/email-validator": "^2.0", + "egulias/email-validator": "^2.1.10|^3", "symfony/asset": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", + "symfony/error-handler": "^4.4|^5.0", "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/form": "^4.3|^5.0", + "symfony/form": "^4.4.17", "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^3.4|^4.0|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/intl": "^4.4|^5.0", "symfony/mime": "^4.3|^5.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/routing": "^3.4|^4.0|^5.0", @@ -35,18 +38,21 @@ "symfony/translation": "^4.2.1|^5.0", "symfony/yaml": "^3.4|^4.0|^5.0", "symfony/security-acl": "^2.8|^3.0", + "symfony/security-core": "^3.0|^4.0|^5.0", "symfony/security-csrf": "^3.4|^4.0|^5.0", "symfony/security-http": "^3.4|^4.0|^5.0", "symfony/stopwatch": "^3.4|^4.0|^5.0", "symfony/console": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^3.4|^4.0|^5.0", "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/web-link": "^3.4|^4.0|^5.0", - "symfony/workflow": "^4.3|^5.0" + "symfony/web-link": "^4.4|^5.0", + "symfony/workflow": "^4.3|^5.0", + "twig/cssinliner-extra": "^2.12|^3", + "twig/inky-extra": "^2.12|^3", + "twig/markdown-extra": "^2.12|^3" }, "conflict": { "symfony/console": "<3.4", - "symfony/form": "<4.3", + "symfony/form": "<4.4", "symfony/http-foundation": "<4.3", "symfony/translation": "<4.2", "symfony/workflow": "<4.3" @@ -74,10 +80,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/DebugBundle/.gitattributes b/src/Symfony/Bundle/DebugBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php b/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php index ae69edfff759d..7df85c70c90e7 100644 --- a/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php +++ b/src/Symfony/Bundle/DebugBundle/Command/ServerDumpPlaceholderCommand.php @@ -43,7 +43,7 @@ protected function configure() $this->setDescription($this->replacedCommand->getDescription()); } - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { (new SymfonyStyle($input, $output))->getErrorStyle()->warning('In order to use the VarDumper server, set the "debug.dump_destination" config option to "tcp://%env(VAR_DUMPER_SERVER)%"'); diff --git a/src/Symfony/Bundle/DebugBundle/DebugBundle.php b/src/Symfony/Bundle/DebugBundle/DebugBundle.php index 04fd507612747..fd7d8e72d6a4c 100644 --- a/src/Symfony/Bundle/DebugBundle/DebugBundle.php +++ b/src/Symfony/Bundle/DebugBundle/DebugBundle.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\DebugBundle; use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\DumpDataCollectorPass; +use Symfony\Bundle\DebugBundle\DependencyInjection\Compiler\RemoveWebServerBundleLoggerPass; use Symfony\Component\Console\Application; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; @@ -52,6 +53,7 @@ public function build(ContainerBuilder $container) parent::build($container); $container->addCompilerPass(new DumpDataCollectorPass()); + $container->addCompilerPass(new RemoveWebServerBundleLoggerPass()); } public function registerCommands(Application $application) diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.php new file mode 100644 index 0000000000000..2d30cf21d9d6d --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/Compiler/RemoveWebServerBundleLoggerPass.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\Bundle\DebugBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; + +/** + * @author Jérémy Derussé + * + * @internal + */ +final class RemoveWebServerBundleLoggerPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if ($container->hasDefinition('web_server.command.server_log') && $container->hasDefinition('monolog.command.server_log')) { + $container->removeDefinition('web_server.command.server_log'); + } + } +} diff --git a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php index 8309db19ecd7c..7b8e6234709da 100644 --- a/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php +++ b/src/Symfony/Bundle/DebugBundle/DependencyInjection/DebugExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\DebugBundle\DependencyInjection; +use Symfony\Bridge\Monolog\Command\ServerLogCommand; use Symfony\Bundle\DebugBundle\Command\ServerDumpPlaceholderCommand; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -58,7 +59,7 @@ public function load(array $configs, ContainerBuilder $container) $container->getDefinition('var_dumper.command.server_dump') ->setClass(ServerDumpPlaceholderCommand::class) ; - } elseif (0 === strpos($config['dump_destination'], 'tcp://')) { + } elseif (str_starts_with($config['dump_destination'], 'tcp://')) { $container->getDefinition('debug.dump_listener') ->replaceArgument(2, new Reference('var_dumper.server_connection')) ; @@ -90,6 +91,10 @@ public function load(array $configs, ContainerBuilder $container) ]]) ; } + + if (!class_exists(ServerLogCommand::class)) { + $container->removeDefinition('monolog.command.server_log'); + } } /** diff --git a/src/Symfony/Bundle/DebugBundle/LICENSE b/src/Symfony/Bundle/DebugBundle/LICENSE index cf8b3ebe87145..a843ec124ea70 100644 --- a/src/Symfony/Bundle/DebugBundle/LICENSE +++ b/src/Symfony/Bundle/DebugBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2014-2019 Fabien Potencier +Copyright (c) 2014-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/DebugBundle/README.md b/src/Symfony/Bundle/DebugBundle/README.md new file mode 100644 index 0000000000000..bed2f5b6d680a --- /dev/null +++ b/src/Symfony/Bundle/DebugBundle/README.md @@ -0,0 +1,13 @@ +DebugBundle +=========== + +DebugBundle provides a tight integration of the Symfony VarDumper component and +the ServerLogCommand from MonologBridge into the Symfony full-stack framework. + +Resources +--------- + + * [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/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml index 75819a017bf33..c7cc5725cf8f8 100644 --- a/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/DebugBundle/Resources/config/services.xml @@ -40,6 +40,19 @@ 0 + + + + + + %kernel.charset% + %kernel.project_dir% + + + + + + null %kernel.charset% @@ -83,7 +96,7 @@ - + @@ -94,5 +107,9 @@ + + + + diff --git a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php index c09ed8bc8ce37..1f85a1a31696e 100644 --- a/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php +++ b/src/Symfony/Bundle/DebugBundle/Tests/DependencyInjection/DebugExtensionTest.php @@ -86,6 +86,7 @@ private function compileContainer(ContainerBuilder $container) { $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); } } diff --git a/src/Symfony/Bundle/DebugBundle/composer.json b/src/Symfony/Bundle/DebugBundle/composer.json index 5087c97e08b26..261f3d23c7264 100644 --- a/src/Symfony/Bundle/DebugBundle/composer.json +++ b/src/Symfony/Bundle/DebugBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/debug-bundle", "type": "symfony-bundle", - "description": "Symfony DebugBundle", + "description": "Provides a tight integration of the Symfony VarDumper component and the ServerLogCommand from MonologBridge into the Symfony full-stack framework", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,9 +16,10 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "ext-xml": "*", "symfony/http-kernel": "^3.4|^4.0|^5.0", + "symfony/polyfill-php80": "^1.16", "symfony/twig-bridge": "^3.4|^4.0|^5.0", "symfony/var-dumper": "^4.1.1|^5.0" }, @@ -41,10 +42,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/FrameworkBundle/.gitattributes b/src/Symfony/Bundle/FrameworkBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md index 2800b5987dc85..7a7e049dcfd08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/FrameworkBundle/CHANGELOG.md @@ -4,17 +4,29 @@ CHANGELOG 4.4.0 ----- + * Added `lint:container` command to check that services wiring matches type declarations + * Added `MailerAssertionsTrait` * Deprecated support for `templating` engine in `TemplateController`, use Twig instead * Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` * Deprecated the `controller_name_converter` and `resolve_controller_name_subscriber` services * The `ControllerResolver` and `DelegatingLoader` classes have been marked as `final` * Added support for configuring chained cache pools - * Deprecated booting the kernel before running `WebTestCase::createClient()` + * Deprecated calling `WebTestCase::createClient()` while a kernel has been booted, ensure the kernel is shut down before calling the method + * Deprecated `routing.loader.service`, use `routing.loader.container` instead. + * Not tagging service route loaders with `routing.route_loader` has been deprecated. + * Overriding the methods `KernelTestCase::tearDown()` and `WebTestCase::tearDown()` without the `void` return-type is deprecated. + * Added new `error_controller` configuration to handle system exceptions + * Added sort option for `translation:update` command. + * [BC Break] The `framework.messenger.routing.senders` config key is not deeply merged anymore. + * Added `secrets:*` commands to deal with secrets seamlessly. + * Made `framework.session.handler_id` accept a DSN + * Marked the `RouterDataCollector` class as `@final`. + * [BC Break] The `framework.messenger.buses..middleware` config key is not deeply merged anymore. 4.3.0 ----- - * Deprecated the `framework.templating` option, use Twig instead. + * Deprecated the `framework.templating` option, configure the Twig bundle instead. * Added `WebTestAssertionsTrait` (included by default in `WebTestCase`) * Renamed `Client` to `KernelBrowser` * Not passing the project directory to the constructor of the `AssetsInstallCommand` is deprecated. This argument will diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php index e593002e22851..f1b94bdfcdf91 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AbstractPhpFileCacheWarmer.php @@ -14,11 +14,9 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\HttpKernel\CacheWarmer\CacheWarmerInterface; -/** - * @internal - */ abstract class AbstractPhpFileCacheWarmer implements CacheWarmerInterface { private $phpArrayFile; @@ -46,13 +44,13 @@ public function warmUp($cacheDir) { $arrayAdapter = new ArrayAdapter(); - spl_autoload_register([PhpArrayAdapter::class, 'throwOnRequiredClass']); + spl_autoload_register([ClassExistenceResource::class, 'throwOnRequiredClass']); try { if (!$this->doWarmUp($cacheDir, $arrayAdapter)) { return; } } finally { - spl_autoload_unregister([PhpArrayAdapter::class, 'throwOnRequiredClass']); + spl_autoload_unregister([ClassExistenceResource::class, 'throwOnRequiredClass']); } // the ArrayAdapter stores the values serialized @@ -69,8 +67,18 @@ protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array } /** - * @param string $cacheDir - * @param ArrayAdapter $arrayAdapter + * @internal + */ + final protected function ignoreAutoloadException(string $class, \Exception $exception): void + { + try { + ClassExistenceResource::throwOnRequiredClass($class, $exception); + } catch (\ReflectionException $e) { + } + } + + /** + * @param string $cacheDir * * @return bool false if there is nothing to warm-up */ diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php index 340198e5e2c1b..8ed3f618f902a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/AnnotationsCacheWarmer.php @@ -13,9 +13,11 @@ use Doctrine\Common\Annotations\AnnotationException; use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\Cache\DoctrineProvider; /** @@ -38,7 +40,7 @@ class AnnotationsCacheWarmer extends AbstractPhpFileCacheWarmer public function __construct(Reader $annotationReader, string $phpArrayFile, $excludeRegexp = null, $debug = false) { if ($excludeRegexp instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); $excludeRegexp = $debug; $debug = 4 < \func_num_args() && func_get_arg(4); } @@ -60,7 +62,10 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) } $annotatedClasses = include $annotatedClassPatterns; - $reader = new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter), $this->debug); + $reader = class_exists(PsrCachedReader::class) + ? new PsrCachedReader($this->annotationReader, $arrayAdapter, $this->debug) + : new CachedReader($this->annotationReader, new DoctrineProvider($arrayAdapter), $this->debug) + ; foreach ($annotatedClasses as $class) { if (null !== $this->excludeRegexp && preg_match($this->excludeRegexp, $class)) { @@ -68,34 +73,51 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) } try { $this->readAllComponents($reader, $class); - } catch (\ReflectionException $e) { - // ignore failing reflection - } catch (AnnotationException $e) { - /* - * Ignore any AnnotationException to not break the cache warming process if an Annotation is badly - * configured or could not be found / read / etc. - * - * In particular cases, an Annotation in your code can be used and defined only for a specific - * environment but is always added to the annotations.map file by some Symfony default behaviors, - * and you always end up with a not found Annotation. - */ + } catch (\Exception $e) { + $this->ignoreAutoloadException($class, $e); } } return true; } - private function readAllComponents(Reader $reader, $class) + protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) + { + // make sure we don't cache null values + $values = array_filter($values, function ($val) { return null !== $val; }); + + parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); + } + + private function readAllComponents(Reader $reader, string $class) { $reflectionClass = new \ReflectionClass($class); - $reader->getClassAnnotations($reflectionClass); + + try { + $reader->getClassAnnotations($reflectionClass); + } catch (AnnotationException $e) { + /* + * Ignore any AnnotationException to not break the cache warming process if an Annotation is badly + * configured or could not be found / read / etc. + * + * In particular cases, an Annotation in your code can be used and defined only for a specific + * environment but is always added to the annotations.map file by some Symfony default behaviors, + * and you always end up with a not found Annotation. + */ + } foreach ($reflectionClass->getMethods() as $reflectionMethod) { - $reader->getMethodAnnotations($reflectionMethod); + try { + $reader->getMethodAnnotations($reflectionMethod); + } catch (AnnotationException $e) { + } } foreach ($reflectionClass->getProperties() as $reflectionProperty) { - $reader->getPropertyAnnotations($reflectionProperty); + try { + $reader->getPropertyAnnotations($reflectionProperty); + } catch (AnnotationException $e) { + } } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php index e9eec5e6c7606..61d9f21a5aaf5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/RouterCacheWarmer.php @@ -57,7 +57,7 @@ public function warmUp($cacheDir) * * @return bool always true */ - public function isOptional() + public function isOptional(): bool { return true; } @@ -65,7 +65,7 @@ public function isOptional() /** * {@inheritdoc} */ - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return [ 'router' => RouterInterface::class, diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php index 41a8aaa04de2a..c73203927db83 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/SerializerCacheWarmer.php @@ -37,7 +37,7 @@ class SerializerCacheWarmer extends AbstractPhpFileCacheWarmer public function __construct(array $loaders, string $phpArrayFile) { if (2 < \func_num_args() && func_get_arg(2) instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); } parent::__construct($phpArrayFile); $this->loaders = $loaders; @@ -58,10 +58,10 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) foreach ($loader->getMappedClasses() as $mappedClass) { try { $metadataFactory->getMetadataFor($mappedClass); - } catch (\ReflectionException $e) { - // ignore failing reflection } catch (AnnotationException $e) { // ignore failing annotations + } catch (\Exception $e) { + $this->ignoreAutoloadException($mappedClass, $e); } } } @@ -74,7 +74,7 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) * * @return XmlFileLoader[]|YamlFileLoader[] */ - private function extractSupportedLoaders(array $loaders) + private function extractSupportedLoaders(array $loaders): array { $supportedLoaders = []; diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php index 6e5a11cade4a7..80099b44ae58a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinder.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; -@trigger_error('The '.TemplateFinder::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateFinder::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Finder\Finder; use Symfony\Component\HttpKernel\Bundle\BundleInterface; @@ -34,9 +34,7 @@ class TemplateFinder implements TemplateFinderInterface private $templates; /** - * @param KernelInterface $kernel A KernelInterface instance - * @param TemplateNameParserInterface $parser A TemplateNameParserInterface instance - * @param string $rootDir The directory where global templates can be stored + * @param string $rootDir The directory where global templates can be stored */ public function __construct(KernelInterface $kernel, TemplateNameParserInterface $parser, string $rootDir) { @@ -70,11 +68,9 @@ public function findAllTemplates() /** * Find templates in the given directory. * - * @param string $dir The folder where to look for templates - * * @return TemplateReferenceInterface[] */ - private function findTemplatesInFolder($dir) + private function findTemplatesInFolder(string $dir): array { $templates = []; @@ -98,7 +94,7 @@ private function findTemplatesInFolder($dir) * * @return TemplateReferenceInterface[] */ - private function findTemplatesInBundle(BundleInterface $bundle) + private function findTemplatesInBundle(BundleInterface $bundle): array { $name = $bundle->getName(); $templates = array_unique(array_merge( diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php index f5ed025facfdb..51dc25942d3ae 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplateFinderInterface.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; -@trigger_error('The '.TemplateFinderInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateFinderInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); /** * Interface for finding all the templates. diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php index 9db00d828b64e..80deb31069835 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/TemplatePathsCacheWarmer.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\CacheWarmer; -@trigger_error('The '.TemplatePathsCacheWarmer::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplatePathsCacheWarmer::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Bundle\FrameworkBundle\Templating\Loader\TemplateLocator; use Symfony\Component\Filesystem\Filesystem; diff --git a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php index 36e4bb185deb3..ec2c8df4ae5eb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php +++ b/src/Symfony/Bundle/FrameworkBundle/CacheWarmer/ValidatorCacheWarmer.php @@ -15,7 +15,6 @@ use Psr\Cache\CacheItemPoolInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Validator\Mapping\Cache\Psr6Cache; use Symfony\Component\Validator\Mapping\Factory\LazyLoadingMetadataFactory; use Symfony\Component\Validator\Mapping\Loader\LoaderChain; use Symfony\Component\Validator\Mapping\Loader\LoaderInterface; @@ -40,10 +39,10 @@ class ValidatorCacheWarmer extends AbstractPhpFileCacheWarmer public function __construct($validatorBuilder, string $phpArrayFile) { if (!$validatorBuilder instanceof ValidatorBuilder && !$validatorBuilder instanceof ValidatorBuilderInterface) { - throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, ValidatorBuilder::class, \is_object($validatorBuilder) ? \get_class($validatorBuilder) : \gettype($validatorBuilder))); + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, ValidatorBuilder::class, \is_object($validatorBuilder) ? \get_class($validatorBuilder) : \gettype($validatorBuilder))); } if (2 < \func_num_args() && func_get_arg(2) instanceof CacheItemPoolInterface) { - @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The CacheItemPoolInterface $fallbackPool argument of "%s()" is deprecated since Symfony 4.2, you should not pass it anymore.', __METHOD__), \E_USER_DEPRECATED); } parent::__construct($phpArrayFile); $this->validatorBuilder = $validatorBuilder; @@ -59,7 +58,7 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) } $loaders = $this->validatorBuilder->getLoaders(); - $metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), new Psr6Cache($arrayAdapter)); + $metadataFactory = new LazyLoadingMetadataFactory(new LoaderChain($loaders), $arrayAdapter); foreach ($this->extractSupportedLoaders($loaders) as $loader) { foreach ($loader->getMappedClasses() as $mappedClass) { @@ -67,10 +66,10 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) if ($metadataFactory->hasMetadataFor($mappedClass)) { $metadataFactory->getMetadataFor($mappedClass); } - } catch (\ReflectionException $e) { - // ignore failing reflection } catch (AnnotationException $e) { // ignore failing annotations + } catch (\Exception $e) { + $this->ignoreAutoloadException($mappedClass, $e); } } } @@ -81,7 +80,9 @@ protected function doWarmUp($cacheDir, ArrayAdapter $arrayAdapter) protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array $values) { // make sure we don't cache null values - parent::warmUpPhpArrayAdapter($phpArrayAdapter, array_filter($values)); + $values = array_filter($values, function ($val) { return null !== $val; }); + + parent::warmUpPhpArrayAdapter($phpArrayAdapter, $values); } /** @@ -89,7 +90,7 @@ protected function warmUpPhpArrayAdapter(PhpArrayAdapter $phpArrayAdapter, array * * @return XmlFileLoader[]|YamlFileLoader[] */ - private function extractSupportedLoaders(array $loaders) + private function extractSupportedLoaders(array $loaders): array { $supportedLoaders = []; diff --git a/src/Symfony/Bundle/FrameworkBundle/Client.php b/src/Symfony/Bundle/FrameworkBundle/Client.php index 3e395a4904f4d..97b19cce3a499 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Client.php +++ b/src/Symfony/Bundle/FrameworkBundle/Client.php @@ -62,7 +62,7 @@ public function getKernel() /** * Gets the profile associated with the current Response. * - * @return HttpProfile|false A Profile instance + * @return HttpProfile|false|null A Profile instance */ public function getProfile() { @@ -116,6 +116,7 @@ protected function doRequest($request) // avoid shutting down the Kernel if no request has been performed yet // WebTestCase::createClient() boots the Kernel but do not handle a request if ($this->hasPerformedRequest && $this->reboot) { + $this->kernel->boot(); $this->kernel->shutdown(); } else { $this->hasPerformedRequest = true; @@ -167,9 +168,9 @@ protected function getScript($request) $requires = ''; foreach (get_declared_classes() as $class) { - if (0 === strpos($class, 'ComposerAutoloaderInit')) { + if (str_starts_with($class, 'ComposerAutoloaderInit')) { $r = new \ReflectionClass($class); - $file = \dirname(\dirname($r->getFileName())).'/autoload.php'; + $file = \dirname($r->getFileName(), 2).'/autoload.php'; if (file_exists($file)) { $requires .= 'require_once '.var_export($file, true).";\n"; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php index 7753f1b25f388..23cd6ac8eceb3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AboutCommand.php @@ -37,7 +37,7 @@ class AboutCommand extends Command protected function configure() { $this - ->setDescription('Displays information about the current project') + ->setDescription('Display information about the current project') ->setHelp(<<<'EOT' The %command.name% command displays information about the current Symfony project. @@ -54,7 +54,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -80,12 +80,12 @@ protected function execute(InputInterface $input, OutputInterface $output) new TableSeparator(), ['PHP'], new TableSeparator(), - ['Version', PHP_VERSION], - ['Architecture', (PHP_INT_SIZE * 8).' bits'], - ['Intl locale', class_exists('Locale', false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a'], + ['Version', \PHP_VERSION], + ['Architecture', (\PHP_INT_SIZE * 8).' bits'], + ['Intl locale', class_exists(\Locale::class, false) && \Locale::getDefault() ? \Locale::getDefault() : 'n/a'], ['Timezone', date_default_timezone_get().' ('.(new \DateTime())->format(\DateTime::W3C).')'], - ['OPcache', \extension_loaded('Zend OPcache') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'], - ['APCu', \extension_loaded('apcu') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'], + ['OPcache', \extension_loaded('Zend OPcache') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'], + ['APCu', \extension_loaded('apcu') && filter_var(\ini_get('apc.enabled'), \FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false'], ['Xdebug', \extension_loaded('xdebug') ? 'true' : 'false'], ]; @@ -100,6 +100,8 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->table([], $rows); + + return 0; } private static function formatPath(string $path, string $baseDir): string @@ -114,7 +116,9 @@ private static function formatFileSize(string $path): string } else { $size = 0; foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($path, \RecursiveDirectoryIterator::SKIP_DOTS | \RecursiveDirectoryIterator::FOLLOW_SYMLINKS)) as $file) { - $size += $file->getSize(); + if ($file->isReadable()) { + $size += $file->getSize(); + } } } @@ -123,7 +127,7 @@ private static function formatFileSize(string $path): string private static function isExpired(string $date): bool { - $date = \DateTime::createFromFormat('m/Y', $date); + $date = \DateTime::createFromFormat('d/m/Y', '01/'.$date); return false !== $date && new \DateTime() > $date->modify('last day of this month 23:59:59'); } @@ -131,9 +135,9 @@ private static function isExpired(string $date): bool private static function getDotenvVars(): array { $vars = []; - foreach (explode(',', getenv('SYMFONY_DOTENV_VARS')) as $name) { - if ('' !== $name && false !== $value = getenv($name)) { - $vars[$name] = $value; + foreach (explode(',', $_SERVER['SYMFONY_DOTENV_VARS'] ?? $_ENV['SYMFONY_DOTENV_VARS'] ?? '') as $name) { + if ('' !== $name && isset($_ENV[$name])) { + $vars[$name] = $_ENV[$name]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php index fe0d60b5554ff..0ff799ba9bbc3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AbstractConfigCommand.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component 10000 \Console\Exception\LogicException; use Symfony\Component\Console\Helper\Table; +use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\StyleInterface; use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; @@ -26,6 +27,9 @@ */ abstract class AbstractConfigCommand extends ContainerDebugCommand { + /** + * @param OutputInterface|StyleInterface $output + */ protected function listBundles($output) { $title = 'Available registered bundles with their extension alias if available'; @@ -58,7 +62,7 @@ protected function listBundles($output) protected function findExtension($name) { $bundles = $this->initializeBundles(); - $minScore = INF; + $minScore = \INF; foreach ($bundles as $bundle) { if ($name === $bundle->getName()) { @@ -75,24 +79,24 @@ protected function findExtension($name) $guess = $bundle->getName(); $minScore = $distance; } + } - $extension = $bundle->getContainerExtension(); + $container = $this->getContainerBuilder(); - if ($extension) { - if ($name === $extension->getAlias()) { - return $extension; - } + if ($container->hasExtension($name)) { + return $container->getExtension($name); + } - $distance = levenshtein($name, $extension->getAlias()); + foreach ($container->getExtensions() as $extension) { + $distance = levenshtein($name, $extension->getAlias()); - if ($distance < $minScore) { - $guess = $extension->getAlias(); - $minScore = $distance; - } + if ($distance < $minScore) { + $guess = $extension->getAlias(); + $minScore = $distance; } } - if ('Bundle' !== substr($name, -6)) { + if (!str_ends_with($name, 'Bundle')) { $message = sprintf('No extensions with configuration available for "%s".', $name); } else { $message = sprintf('No extension with alias "%s" is enabled.', $name); @@ -108,11 +112,11 @@ protected function findExtension($name) public function validateConfiguration(ExtensionInterface $extension, $configuration) { if (!$configuration) { - throw new \LogicException(sprintf('The extension with alias "%s" does not have its getConfiguration() method setup', $extension->getAlias())); + throw new \LogicException(sprintf('The extension with alias "%s" does not have its getConfiguration() method setup.', $extension->getAlias())); } if (!$configuration instanceof ConfigurationInterface) { - throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable', \get_class($configuration))); + throw new \LogicException(sprintf('Configuration class "%s" should implement ConfigurationInterface in order to be dumpable.', \get_class($configuration))); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php index ff7352790cef3..c3e1456b3863d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/AssetsInstallCommand.php @@ -35,9 +35,9 @@ */ class AssetsInstallCommand extends Command { - const METHOD_COPY = 'copy'; - const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink'; - const METHOD_RELATIVE_SYMLINK = 'relative symlink'; + public const METHOD_COPY = 'copy'; + public const METHOD_ABSOLUTE_SYMLINK = 'absolute symlink'; + public const METHOD_RELATIVE_SYMLINK = 'relative symlink'; protected static $defaultName = 'assets:install'; @@ -49,7 +49,7 @@ public function __construct(Filesystem $filesystem, string $projectDir = null) parent::__construct(); if (null === $projectDir) { - @trigger_error(sprintf('Not passing the project directory to the constructor of %s is deprecated since Symfony 4.3 and will not be supported in 5.0.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('Not passing the project directory to the constructor of %s is deprecated since Symfony 4.3 and will not be supported in 5.0.', __CLASS__), \E_USER_DEPRECATED); } $this->filesystem = $filesystem; @@ -65,10 +65,10 @@ protected function configure() ->setDefinition([ new InputArgument('target', InputArgument::OPTIONAL, 'The target directory', null), ]) - ->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlinks the assets instead of copying it') + ->addOption('symlink', null, InputOption::VALUE_NONE, 'Symlink the assets instead of copying them') ->addOption('relative', null, InputOption::VALUE_NONE, 'Make relative symlinks') ->addOption('no-cleanup', null, InputOption::VALUE_NONE, 'Do not remove the assets of the bundles that no longer exist') - ->setDescription('Installs bundles web assets under a public directory') + ->setDescription('Install bundle\'s web assets under a public directory') ->setHelp(<<<'EOT' The %command.name% command installs bundle assets into a given directory (e.g. the public directory). @@ -95,12 +95,11 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { /** @var KernelInterface $kernel */ $kernel = $this->getApplication()->getKernel(); - $targetArg = rtrim($input->getArgument('target'), '/'); - + $targetArg = rtrim($input->getArgument('target') ?? '', '/'); if (!$targetArg) { $targetArg = $this->getPublicDirectory($kernel->getContainer()); } @@ -109,7 +108,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $targetArg = $kernel->getProjectDir().'/'.$targetArg; if (!is_dir($targetArg)) { - throw new InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $input->getArgument('target'))); + throw new InvalidArgumentException(sprintf('The target directory "%s" does not exist.', $targetArg)); } } @@ -137,13 +136,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $validAssetDirs = []; /** @var BundleInterface $bundle */ foreach ($kernel->getBundles() as $bundle) { - if (!method_exists($bundle, 'getPublicDir')) { - @trigger_error(sprintf('Not defining "getPublicDir()" method in the "%s" class is deprecated since Symfony 4.4 and will not be supported in 5.0.', \get_class($bundle)), E_USER_DEPRECATED); - $publicDir = 'Resources/public'; - } else { - $publicDir = ltrim($bundle->getPublicDir(), '\\/'); - } - if (!is_dir($originDir = $bundle->getPath().\DIRECTORY_SEPARATOR.$publicDir)) { + if (!is_dir($originDir = $bundle->getPath().'/Resources/public') && !is_dir($originDir = $bundle->getPath().'/public')) { continue; } @@ -268,7 +261,7 @@ private function hardCopy(string $originDir, string $targetDir): string return self::METHOD_COPY; } - private function getPublicDirectory(ContainerInterface $container) + private function getPublicDirectory(ContainerInterface $container): string { $defaultPublicDir = 'public'; @@ -284,10 +277,6 @@ private function getPublicDirectory(ContainerInterface $container) $composerConfig = json_decode(file_get_contents($composerFilePath), true); - if (isset($composerConfig['extra']['public-dir'])) { - return $composerConfig['extra']['public-dir']; - } - - return $defaultPublicDir; + return $composerConfig['extra']['public-dir'] ?? $defaultPublicDir; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php index 24aa8171ca5ef..d112c9b086c0f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheClearCommand.php @@ -44,7 +44,7 @@ public function __construct(CacheClearerInterface $cacheClearer, Filesystem $fil parent::__construct(); $this->cacheClearer = $cacheClearer; - $this->filesystem = $filesystem ?: new Filesystem(); + $this->filesystem = $filesystem ?? new Filesystem(); } /** @@ -57,9 +57,9 @@ protected function configure() new InputOption('no-warmup', '', InputOption::VALUE_NONE, 'Do not warm up the cache'), new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) - ->setDescription('Clears the cache') + ->setDescription('Clear the cache') ->setHelp(<<<'EOF' -The %command.name% command clears the application cache for a given environment +The %command.name% command clears and warms up the application cache for a given environment and debug mode: php %command.full_name% --env=dev @@ -72,7 +72,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $fs = $this->filesystem; $io = new SymfonyStyle($input, $output); @@ -81,11 +81,11 @@ protected function execute(InputInterface $input, OutputInterface $output) $realCacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir'); // the old cache dir name must not be longer than the real one to avoid exceeding // the maximum length of a directory or file path within it (esp. Windows MAX_PATH) - $oldCacheDir = substr($realCacheDir, 0, -1).('~' === substr($realCacheDir, -1) ? '+' : '~'); + $oldCacheDir = substr($realCacheDir, 0, -1).(str_ends_with($realCacheDir, '~') ? '+' : '~'); $fs->remove($oldCacheDir); if (!is_writable($realCacheDir)) { - throw new RuntimeException(sprintf('Unable to write in the "%s" directory', $realCacheDir)); + throw new RuntimeException(sprintf('Unable to write in the "%s" directory.', $realCacheDir)); } $io->comment(sprintf('Clearing the cache for the %s environment with debug %s', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); @@ -142,7 +142,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $mount = implode(' ', $mount).'/'; - if (0 === strpos($realCacheDir, $mount)) { + if (str_starts_with($realCacheDir, $mount)) { $io->note('For better performances, you should move the cache and log directories to a non-shared folder of the VM.'); $oldCacheDir = false; break; @@ -175,6 +175,8 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->success(sprintf('Cache for the "%s" environment (debug=%s) was successfully cleared.', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); + + return 0; } private function warmup(string $warmupDir, string $realCacheDir, bool $enableOptionalWarmers = true) diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php index dc42910d34325..123617e58b189 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolClearCommand.php @@ -47,7 +47,7 @@ protected function configure() ->setDefinition([ new InputArgument('pools', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'A list of cache pools or cache pool clearers'), ]) - ->setDescription('Clears cache pools') + ->setDescription('Clear cache pools') ->setHelp(<<<'EOF' The %command.name% command clears the given cache pools or cache pool clearers. @@ -60,7 +60,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $kernel = $this->getApplication()->getKernel(); @@ -99,5 +99,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->success('Cache was successfully cleared.'); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php index 656f9a9de302d..922ec2dd7e94b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolDeleteCommand.php @@ -46,7 +46,7 @@ protected function configure() new InputArgument('pool', InputArgument::REQUIRED, 'The cache pool from which to delete an item'), new InputArgument('key', InputArgument::REQUIRED, 'The cache key to delete from the pool'), ]) - ->setDescription('Deletes an item from a cache pool') + ->setDescription('Delete an item from a cache pool') ->setHelp(<<<'EOF' The %command.name% deletes an item from a given cache pool. @@ -59,7 +59,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $pool = $input->getArgument('pool'); @@ -69,7 +69,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$cachePool->hasItem($key)) { $io->note(sprintf('Cache item "%s" does not exist in cache pool "%s".', $key, $pool)); - return; + return 0; } if (!$cachePool->deleteItem($key)) { @@ -77,5 +77,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->success(sprintf('Cache item "%s" was successfully deleted.', $key)); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php index 4f399ab61556a..7b725411d5015 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolListCommand.php @@ -51,12 +51,14 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $io->table(['Pool name'], array_map(function ($pool) { return [$pool]; }, $this->poolNames)); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php index 74b53063784b7..fb9af73064cb4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CachePoolPruneCommand.php @@ -44,7 +44,7 @@ public function __construct(iterable $pools) protected function configure() { $this - ->setDescription('Prunes cache pools') + ->setDescription('Prune cache pools') ->setHelp(<<<'EOF' The %command.name% command deletes all expired items from all pruneable pools. @@ -57,7 +57,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -67,5 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->success('Successfully pruned cache pool(s).'); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php index 04b5d2f7c6b9f..50b51f90734c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/CacheWarmupCommand.php @@ -47,17 +47,12 @@ protected function configure() ->setDefinition([ new InputOption('no-optional-warmers', '', InputOption::VALUE_NONE, 'Skip optional cache warmers (faster)'), ]) - ->setDescription('Warms up an empty cache') + ->setDescription('Warm up an empty cache') ->setHelp(<<<'EOF' The %command.name% command warms up the cache. Before running this command, the cache must be empty. -This command does not generate the classes cache (as when executing this -command, too many classes that should be part of the cache are already loaded -in memory). Use curl or any other similar tool to warm up -the classes cache if you want. - EOF ) ; @@ -66,7 +61,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -80,5 +75,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $this->cacheWarmer->warmUp($kernel->getContainer()->getParameter('kernel.cache_dir')); $io->success(sprintf('Cache for the "%s" environment (debug=%s) was successfully warmed.', $kernel->getEnvironment(), var_export($kernel->isDebug(), true))); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php index 4761b66ad9012..063932dee5782 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDebugCommand.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Command; +use Symfony\Component\Config\Definition\Processor; use Symfony\Component\Console\Exception\LogicException; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -18,6 +19,8 @@ use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DependencyInjection\Compiler\ValidateEnvPlaceholdersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ConfigurationExtensionInterface; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; use Symfony\Component\Yaml\Yaml; /** @@ -41,7 +44,7 @@ protected function configure() new InputArgument('name', InputArgument::OPTIONAL, 'The bundle name or the extension alias'), new InputArgument('path', InputArgument::OPTIONAL, 'The configuration option path'), ]) - ->setDescription('Dumps the current configuration for an extension') + ->setDescription('Dump the current configuration for an extension') ->setHelp(<<<'EOF' The %command.name% command dumps the current configuration for an extension/bundle. @@ -63,7 +66,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -73,35 +76,27 @@ protected function execute(InputInterface $input, OutputInterface $output) $errorIo->comment('Provide the name of a bundle as the first argument of this command to dump its configuration. (e.g. debug:config FrameworkBundle)'); $errorIo->comment('For dumping a specific option, add its path as the second argument of this command. (e.g. debug:config FrameworkBundle serializer to dump the framework.serializer configuration)'); - return; + return 0; } $extension = $this->findExtension($name); - $container = $this->compileContainer(); - $extensionAlias = $extension->getAlias(); - $extensionConfig = []; - foreach ($container->getCompilerPassConfig()->getPasses() as $pass) { - if ($pass instanceof ValidateEnvPlaceholdersPass) { - $extensionConfig = $pass->getExtensionConfig(); - break; - } - } - - if (!isset($extensionConfig[$extensionAlias])) { - throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias)); - } + $container = $this->compileContainer(); - $config = $container->resolveEnvPlaceholders($extensionConfig[$extensionAlias]); + $config = $container->resolveEnvPlaceholders( + $container->getParameterBag()->resolveValue( + $this->getConfigForExtension($extension, $container) + ) + ); if (null === $path = $input->getArgument('path')) { $io->title( - sprintf('Current configuration for %s', ($name === $extensionAlias ? sprintf('extension with alias "%s"', $extensionAlias) : sprintf('"%s"', $name))) + sprintf('Current configuration for %s', $name === $extensionAlias ? sprintf('extension with alias "%s"', $extensionAlias) : sprintf('"%s"', $name)) ); $io->writeln(Yaml::dump([$extensionAlias => $config], 10)); - return; + return 0; } try { @@ -109,12 +104,14 @@ protected function execute(InputInterface $input, OutputInterface $output) } catch (LogicException $e) { $errorIo->error($e->getMessage()); - return; + return 1; } $io->title(sprintf('Current configuration for "%s.%s"', $extensionAlias, $path)); $io->writeln(Yaml::dump($config, 10)); + + return 0; } private function compileContainer(): ContainerBuilder @@ -143,7 +140,7 @@ private function getConfigForPath(array $config, string $path, string $alias) foreach ($steps as $step) { if (!\array_key_exists($step, $config)) { - throw new LogicException(sprintf('Unable to find configuration for "%s.%s"', $alias, $path)); + throw new LogicException(sprintf('Unable to find configuration for "%s.%s".', $alias, $path)); } $config = $config[$step]; @@ -151,4 +148,33 @@ private function getConfigForPath(array $config, string $path, string $alias) return $config; } + + private function getConfigForExtension(ExtensionInterface $extension, ContainerBuilder $container): array + { + $extensionAlias = $extension->getAlias(); + + $extensionConfig = []; + foreach ($container->getCompilerPassConfig()->getPasses() as $pass) { + if ($pass instanceof ValidateEnvPlaceholdersPass) { + $extensionConfig = $pass->getExtensionConfig(); + break; + } + } + + if (isset($extensionConfig[$extensionAlias])) { + return $extensionConfig[$extensionAlias]; + } + + // Fall back to default config if the extension has one + + if (!$extension instanceof ConfigurationExtensionInterface) { + throw new \LogicException(sprintf('The extension with alias "%s" does not have configuration.', $extensionAlias)); + } + + $configs = $container->getExtensionConfig($extensionAlias); + $configuration = $extension->getConfiguration($configs, $container); + $this->validateConfiguration($extension, $configuration); + + return (new Processor())->processConfiguration($configuration, $configs); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php index 8640c0bacc487..17690f7c99401 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ConfigDumpReferenceCommand.php @@ -44,7 +44,7 @@ protected function configure() new InputArgument('path', InputArgument::OPTIONAL, 'The configuration option path'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (yaml or xml)', 'yaml'), ]) - ->setDescription('Dumps the default configuration for an extension') + ->setDescription('Dump the default configuration for an extension') ->setHelp(<<<'EOF' The %command.name% command dumps the default configuration for an extension/bundle. @@ -74,7 +74,7 @@ protected function configure() * * @throws \LogicException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -86,7 +86,7 @@ protected function execute(InputInterface $input, OutputInterface $output) 'For dumping a specific option, add its path as the second argument of this command. (e.g. config:dump-reference FrameworkBundle profiler.matcher to dump the framework.profiler.matcher configuration)', ]); - return; + return 0; } $extension = $this->findExtension($name); @@ -129,5 +129,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->writeln(null === $path ? $dumper->dump($configuration, $extension->getNamespace()) : $dumper->dumpAtPath($configuration, $path)); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php index 7b01a55ef8af5..ae7e928b1ff79 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerAwareCommand.php @@ -15,7 +15,7 @@ use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" with dependency injection instead.', ContainerAwareCommand::class, Command::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" with dependency injection instead.', ContainerAwareCommand::class, Command::class), \E_USER_DEPRECATED); /** * Command. diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php index 11c189d4b1b8d..c75ba398a67a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerDebugCommand.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; @@ -50,20 +51,20 @@ protected function configure() $this ->setDefinition([ new InputArgument('name', InputArgument::OPTIONAL, 'A service name (foo)'), - new InputOption('show-private', null, InputOption::VALUE_NONE, 'Used to show public *and* private services (deprecated)'), - new InputOption('show-arguments', null, InputOption::VALUE_NONE, 'Used to show arguments in services'), - new InputOption('show-hidden', null, InputOption::VALUE_NONE, 'Used to show hidden (internal) services'), - new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Shows all services with a specific tag'), - new InputOption('tags', null, InputOption::VALUE_NONE, 'Displays tagged services for an application'), - new InputOption('parameter', null, InputOption::VALUE_REQUIRED, 'Displays a specific parameter for an application'), - new InputOption('parameters', null, InputOption::VALUE_NONE, 'Displays parameters for an application'), - new InputOption('types', null, InputOption::VALUE_NONE, 'Displays types (classes/interfaces) available in the container'), - new InputOption('env-var', null, InputOption::VALUE_REQUIRED, 'Displays a specific environment variable used in the container'), - new InputOption('env-vars', null, InputOption::VALUE_NONE, 'Displays environment variables used in the container'), + new InputOption('show-private', null, InputOption::VALUE_NONE, 'Show public *and* private services (deprecated)'), + new InputOption('show-arguments', null, InputOption::VALUE_NONE, 'Show arguments in services'), + new InputOption('show-hidden', null, InputOption::VALUE_NONE, 'Show hidden (internal) services'), + new InputOption('tag', null, InputOption::VALUE_REQUIRED, 'Show all services with a specific tag'), + new InputOption('tags', null, InputOption::VALUE_NONE, 'Display tagged services for an application'), + new InputOption('parameter', null, InputOption::VALUE_REQUIRED, 'Display a specific parameter for an application'), + new InputOption('parameters', null, InputOption::VALUE_NONE, 'Display parameters for an application'), + new InputOption('types', null, InputOption::VALUE_NONE, 'Display types (classes/interfaces) available in the container'), + new InputOption('env-var', null, InputOption::VALUE_REQUIRED, 'Display a specific environment variable used in the container'), + new InputOption('env-vars', null, InputOption::VALUE_NONE, 'Display environment variables used in the container'), new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), ]) - ->setDescription('Displays current services for an application') + ->setDescription('Display current services for an application') ->setHelp(<<<'EOF' The %command.name% command displays all configured public services: @@ -73,6 +74,10 @@ protected function configure() php %command.full_name% validator +To get specific information about a service including all its arguments, use the --show-arguments flag: + + php %command.full_name% validator --show-arguments + To see available types that can be used for autowiring, use the --types flag: php %command.full_name% --types @@ -114,10 +119,10 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { if ($input->getOption('show-private')) { - @trigger_error('The "--show-private" option no longer has any effect and is deprecated since Symfony 4.1.', E_USER_DEPRECATED); + @trigger_error('The "--show-private" option no longer has any effect and is deprecated since Symfony 4.1.', \E_USER_DEPRECATED); } $io = new SymfonyStyle($input, $output); @@ -184,6 +189,8 @@ protected function execute(InputInterface $input, OutputInterface $output) $errorIo->comment('To search for a specific service, re-run this command with a search term. (e.g. debug:container log)'); } } + + return 0; } /** @@ -213,11 +220,9 @@ protected function validateInput(InputInterface $input) /** * Loads the ContainerBuilder from the cache. * - * @return ContainerBuilder - * * @throws \LogicException */ - protected function getContainerBuilder() + protected function getContainerBuilder(): ContainerBuilder { if ($this->containerBuilder) { return $this->containerBuilder; @@ -229,15 +234,18 @@ protected function getContainerBuilder() $buildContainer = \Closure::bind(function () { return $this->buildContainer(); }, $kernel, \get_class($kernel)); $container = $buildContainer(); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); } else { (new XmlFileLoader($container = new ContainerBuilder(), new FileLocator()))->load($kernel->getContainer()->getParameter('debug.container.dump')); + $locatorPass = new ServiceLocatorTagPass(); + $locatorPass->process($container); } return $this->containerBuilder = $container; } - private function findProperServiceName(InputInterface $input, SymfonyStyle $io, ContainerBuilder $builder, string $name, bool $showHidden) + private function findProperServiceName(InputInterface $input, SymfonyStyle $io, ContainerBuilder $builder, string $name, bool $showHidden): string { $name = ltrim($name, '\\'); @@ -257,12 +265,12 @@ private function findProperServiceName(InputInterface $input, SymfonyStyle $io, return $io->choice('Select one of the following services to display its information', $matchingServices); } - private function findServiceIdsContaining(ContainerBuilder $builder, string $name, bool $showHidden) + private function findServiceIdsContaining(ContainerBuilder $builder, string $name, bool $showHidden): array { $serviceIds = $builder->getServiceIds(); $foundServiceIds = $foundServiceIdsIgnoringBackslashes = []; foreach ($serviceIds as $serviceId) { - if (!$showHidden && 0 === strpos($serviceId, '.')) { + if (!$showHidden && str_starts_with($serviceId, '.')) { continue; } if (false !== stripos(str_replace('\\', '', $serviceId), $name)) { @@ -279,7 +287,7 @@ private function findServiceIdsContaining(ContainerBuilder $builder, string $nam /** * @internal */ - public function filterToServiceTypes($serviceId) + public function filterToServiceTypes(string $serviceId): bool { // filter out things that could not be valid class names if (!preg_match('/(?(DEFINE)(?[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*+))^(?&V)(?:\\\\(?&V))*+(?: \$(?&V))?$/', $serviceId)) { @@ -287,7 +295,7 @@ public function filterToServiceTypes($serviceId) } // if the id has a \, assume it is a class - if (false !== strpos($serviceId, '\\')) { + if (str_contains($serviceId, '\\')) { return true; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php new file mode 100644 index 0000000000000..8225825ae360d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/ContainerLintCommand.php @@ -0,0 +1,120 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Component\Config\ConfigCache; +use Symfony\Component\Config\FileLocator; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Exception\RuntimeException; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; +use Symfony\Component\HttpKernel\Kernel; + +final class ContainerLintCommand extends Command +{ + protected static $defaultName = 'lint:container'; + + /** + * @var ContainerBuilder + */ + private $containerBuilder; + + /** + * {@inheritdoc} + */ + protected function configure() + { + $this + ->setDescription('Ensure that arguments injected into services match type declarations') + ->setHelp('This command parses service definitions and ensures that injected values match the type declarations of each services\' class.') + ; + } + + /** + * {@inheritdoc} + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output); + $errorIo = $io->getErrorStyle(); + + try { + $container = $this->getContainerBuilder(); + } catch (RuntimeException $e) { + $errorIo->error($e->getMessage()); + + return 2; + } + + $container->setParameter('container.build_time', time()); + + $container->compile(); + + return 0; + } + + private function getContainerBuilder(): ContainerBuilder + { + if ($this->containerBuilder) { + return $this->containerBuilder; + } + + $kernel = $this->getApplication()->getKernel(); + $kernelContainer = $kernel->getContainer(); + + if (!$kernel->isDebug() || !(new ConfigCache($kernelContainer->getParameter('debug.container.dump'), true))->isFresh()) { + if (!$kernel instanceof Kernel) { + throw new RuntimeException(sprintf('This command does not support the application kernel: "%s" does not extend "%s".', \get_class($kernel), Kernel::class)); + } + + $buildContainer = \Closure::bind(function (): ContainerBuilder { + $this->initializeBundles(); + + return $this->buildContainer(); + }, $kernel, \get_class($kernel)); + $container = $buildContainer(); + + $skippedIds = []; + } else { + if (!$kernelContainer instanceof Container) { + throw new RuntimeException(sprintf('This command does not support the application container: "%s" does not extend "%s".', \get_class($kernelContainer), Container::class)); + } + + (new XmlFileLoader($container = new ContainerBuilder($parameterBag = new EnvPlaceholderParameterBag()), new FileLocator()))->load($kernelContainer->getParameter('debug.container.dump')); + + $refl = new \ReflectionProperty($parameterBag, 'resolved'); + $refl->setAccessible(true); + $refl->setValue($parameterBag, true); + + $skippedIds = []; + foreach ($container->getServiceIds() as $serviceId) { + if (str_starts_with($serviceId, '.errored.')) { + $skippedIds[$serviceId] = true; + } + } + } + + $container->setParameter('container.build_hash', 'lint_container'); + $container->setParameter('container.build_id', 'lint_container'); + + $container->addCompilerPass(new CheckTypeDeclarationsPass(true, $skippedIds), PassConfig::TYPE_AFTER_REMOVING, -100); + + return $this->containerBuilder = $container; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php index ac692ee62990c..b5023749a3078 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/DebugAutowiringCommand.php @@ -50,7 +50,7 @@ protected function configure() new InputArgument('search', InputArgument::OPTIONAL, 'A search filter'), new InputOption('all', null, InputOption::VALUE_NONE, 'Show also services that are not aliased'), ]) - ->setDescription('Lists classes/interfaces you can use for autowiring') + ->setDescription('List classes/interfaces you can use for autowiring') ->setHelp(<<<'EOF' The %command.name% command displays the classes and interfaces that you can use as type-hints for autowiring: @@ -69,7 +69,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -80,7 +80,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($search = $input->getArgument('search')) { $serviceIds = array_filter($serviceIds, function ($serviceId) use ($search) { - return false !== stripos(str_replace('\\', '', $serviceId), $search) && 0 !== strpos($serviceId, '.'); + return false !== stripos(str_replace('\\', '', $serviceId), $search) && !str_starts_with($serviceId, '.'); }); if (empty($serviceIds)) { @@ -103,9 +103,10 @@ protected function execute(InputInterface $input, OutputInterface $output) $serviceIdsNb = 0; foreach ($serviceIds as $serviceId) { $text = []; - if (0 !== strpos($serviceId, $previousId)) { + $resolvedServiceId = $serviceId; + if (!str_starts_with($serviceId, $previousId)) { $text[] = ''; - if ('' !== $description = Descriptor::getClassDescription($serviceId, $serviceId)) { + if ('' !== $description = Descriptor::getClassDescription($serviceId, $resolvedServiceId)) { if (isset($hasAlias[$serviceId])) { continue; } @@ -145,6 +146,8 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->newLine(); + + return 0; } private function getFileLink(string $class): string diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php index e61f543e7f780..fd0a1ccb800e7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/EventDispatcherDebugCommand.php @@ -50,7 +50,7 @@ protected function configure() new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw description'), ]) - ->setDescription('Displays configured listeners for an application') + ->setDescription('Display configured listeners for an application') ->setHelp(<<<'EOF' The %command.name% command displays all configured listeners: @@ -69,7 +69,7 @@ protected function configure() * * @throws \LogicException */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -78,7 +78,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!$this->dispatcher->hasListeners($event)) { $io->getErrorStyle()->warning(sprintf('The event "%s" does not have any registered listeners.', $event)); - return; + return 0; } $options = ['event' => $event]; @@ -89,5 +89,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $options['raw_text'] = $input->getOption('raw'); $options['output'] = $io; $helper->describe($io, $this->dispatcher, $options); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php index bad3fa0598a56..7758bc949fb45 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterDebugCommand.php @@ -57,7 +57,7 @@ protected function configure() new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), ]) - ->setDescription('Displays current routes for an application') + ->setDescription('Display current routes for an application') ->setHelp(<<<'EOF' The %command.name% displays the configured routes: @@ -73,7 +73,7 @@ protected function configure() * * @throws InvalidArgumentException When route does not exist */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $name = $input->getArgument('name'); @@ -81,7 +81,21 @@ protected function execute(InputInterface $input, OutputInterface $output) $routes = $this->router->getRouteCollection(); if ($name) { - if (!($route = $routes->get($name)) && $matchingRoutes = $this->findRouteNameContaining($name, $routes)) { + $route = $routes->get($name); + $matchingRoutes = $this->findRouteNameContaining($name, $routes); + + if (!$input->isInteractive() && !$route && \count($matchingRoutes) > 1) { + $helper->describe($io, $this->findRouteContaining($name, $routes), [ + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'show_controllers' => $input->getOption('show-controllers'), + 'output' => $io, + ]); + + return 0; + } + + if (!$route && $matchingRoutes) { $default = 1 === \count($matchingRoutes) ? $matchingRoutes[0] : null; $name = $io->choice('Select one of the matching routes', $matchingRoutes, $default); $route = $routes->get($name); @@ -105,6 +119,8 @@ protected function execute(InputInterface $input, OutputInterface $output) 'output' => $io, ]); } + + return 0; } private function findRouteNameContaining(string $name, RouteCollection $routes): array @@ -118,4 +134,16 @@ private function findRouteNameContaining(string $name, RouteCollection $routes): return $foundRoutesNames; } + + private function findRouteContaining(string $name, RouteCollection $routes): RouteCollection + { + $foundRoutes = new RouteCollection(); + foreach ($routes as $routeName => $route) { + if (false !== stripos($routeName, $name)) { + $foundRoutes->add($routeName, $route); + } + } + + return $foundRoutes; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php index dd40352df7f0e..8dd5b545b40c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/RouterMatchCommand.php @@ -49,11 +49,11 @@ protected function configure() $this ->setDefinition([ new InputArgument('path_info', InputArgument::REQUIRED, 'A path info'), - new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Sets the HTTP method'), - new InputOption('scheme', null, InputOption::VALUE_REQUIRED, 'Sets the URI scheme (usually http or https)'), - new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Sets the URI host'), + new InputOption('method', null, InputOption::VALUE_REQUIRED, 'Set the HTTP method'), + new InputOption('scheme', null, InputOption::VALUE_REQUIRED, 'Set the URI scheme (usually http or https)'), + new InputOption('host', null, InputOption::VALUE_REQUIRED, 'Set the URI host'), ]) - ->setDescription('Helps debug routes by simulating a path info match') + ->setDescription('Help debug routes by simulating a path info match') ->setHelp(<<<'EOF' The %command.name% shows which routes match a given request and which don't and for what reason: @@ -71,7 +71,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -113,5 +113,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.php new file mode 100644 index 0000000000000..6d8820443a2c1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsDecryptToLocalCommand.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\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +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\Style\SymfonyStyle; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsDecryptToLocalCommand extends Command +{ + protected static $defaultName = 'secrets:decrypt-to-local'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Decrypt all secrets and stores them in the local vault.') + ->addOption('force', 'f', InputOption::VALUE_NONE, 'Force overriding of secrets that already exist in the local vault') + ->setHelp(<<<'EOF' +The %command.name% command decrypts all secrets and copies them in the local vault. + + %command.full_name% + +When the option --force is provided, secrets that already exist in the local vault are overriden. + + %command.full_name% --force +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if (null === $this->localVault) { + $io->error('The local vault is disabled.'); + + return 1; + } + + $secrets = $this->vault->list(true); + + if (!$input->getOption('force')) { + foreach ($this->localVault->list() as $k => $v) { + unset($secrets[$k]); + } + } + + foreach ($secrets as $k => $v) { + if (null === $v) { + $io->error($this->vault->getLastMessage() ?? sprintf('Secret "%s" has been skipped as there was an error reading it.', $k)); + continue; + } + + $this->localVault->seal($k, $v); + } + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.php new file mode 100644 index 0000000000000..14e7c51e6df53 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsEncryptFromLocalCommand.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\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsEncryptFromLocalCommand extends Command +{ + protected static $defaultName = 'secrets:encrypt-from-local'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Encrypt all local secrets to the vault.') + ->setHelp(<<<'EOF' +The %command.name% command encrypts all locally overridden secrets to the vault. + + %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + if (null === $this->localVault) { + $io->error('The local vault is disabled.'); + + return 1; + } + + foreach ($this->vault->list(true) as $name => $value) { + $localValue = $this->localVault->reveal($name); + + if (null !== $localValue && $value !== $localValue) { + $this->vault->seal($name, $localValue); + } elseif (null !== $message = $this->localVault->getLastMessage()) { + $io->error($message); + + return 1; + } + } + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.php new file mode 100644 index 0000000000000..f0497815627cd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsGenerateKeysCommand.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\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +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\Style\SymfonyStyle; + +/** + * @author Tobias Schultze + * @author Jérémy Derussé + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsGenerateKeysCommand extends Command +{ + protected static $defaultName = 'secrets:generate-keys'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Generate new encryption keys.') + ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') + ->addOption('rotate', 'r', InputOption::VALUE_NONE, 'Re-encrypt existing secrets with the newly generated keys.') + ->setHelp(<<<'EOF' +The %command.name% command generates a new encryption key. + + %command.full_name% + +If encryption keys already exist, the command must be called with +the --rotate option in order to override those keys and re-encrypt +existing secrets. + + %command.full_name% --rotate +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + $vault = $input->getOption('local') ? $this->localVault : $this->vault; + + if (null === $vault) { + $io->success('The local vault is disabled.'); + + return 1; + } + + if (!$input->getOption('rotate')) { + if ($vault->generateKeys()) { + $io->success($vault->getLastMessage()); + + if ($this->vault === $vault) { + $io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'); + } + + return 0; + } + + $io->warning($vault->getLastMessage()); + + return 1; + } + + $secrets = []; + foreach ($vault->list(true) as $name => $value) { + if (null === $value) { + $io->error($vault->getLastMessage()); + + return 1; + } + + $secrets[$name] = $value; + } + + if (!$vault->generateKeys(true)) { + $io->warning($vault->getLastMessage()); + + return 1; + } + + $io->success($vault->getLastMessage()); + + if ($secrets) { + foreach ($secrets as $name => $value) { + $vault->seal($name, $value); + } + + $io->comment('Existing secrets have been rotated to the new keys.'); + } + + if ($this->vault === $vault) { + $io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'); + } + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.php new file mode 100644 index 0000000000000..4586677f785df --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsListCommand.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\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\Dumper; +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\Style\SymfonyStyle; + +/** + * @author Tobias Schultze + * @author Jérémy Derussé + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsListCommand extends Command +{ + protected static $defaultName = 'secrets:list'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('List all secrets.') + ->addOption('reveal', 'r', InputOption::VALUE_NONE, 'Display decrypted values alongside names') + ->setHelp(<<<'EOF' +The %command.name% command list all stored secrets. + + %command.full_name% + +When the option --reveal is provided, the decrypted secrets are also displayed. + + %command.full_name% --reveal +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + + $io->comment('Use "%env()%" to reference a secret in a config file.'); + + if (!$reveal = $input->getOption('reveal')) { + $io->comment(sprintf('To reveal the secrets run php %s %s --reveal', $_SERVER['PHP_SELF'], $this->getName())); + } + + $secrets = $this->vault->list($reveal); + $localSecrets = null !== $this->localVault ? $this->localVault->list($reveal) : null; + + $rows = []; + + $dump = new Dumper($output); + $dump = static function (?string $v) use ($dump) { + return null === $v ? '******' : $dump($v); + }; + + foreach ($secrets as $name => $value) { + $rows[$name] = [$name, $dump($value)]; + } + + if (null !== $message = $this->vault->getLastMessage()) { + $io->comment($message); + } + + foreach ($localSecrets ?? [] as $name => $value) { + if (isset($rows[$name])) { + $rows[$name][] = $dump($value); + } + } + + if (null !== $this->localVault && null !== $message = $this->localVault->getLastMessage()) { + $io->comment($message); + } + + (new SymfonyStyle($input, $output)) + ->table(['Secret', 'Value'] + (null !== $localSecrets ? [2 => 'Local Value'] : []), $rows); + + $io->comment("Local values override secret values.\nUse secrets:set --local to define them."); + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.php new file mode 100644 index 0000000000000..f4c 10000 40a8fdec8c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsRemoveCommand.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\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * @author Jérémy Derussé + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsRemoveCommand extends Command +{ + protected static $defaultName = 'secrets:remove'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Remove a secret from the vault.') + ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret') + ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') + ->setHelp(<<<'EOF' +The %command.name% command removes a secret from the vault. + + %command.full_name% +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); + $vault = $input->getOption('local') ? $this->localVault : $this->vault; + + if (null === $vault) { + $io->success('The local vault is disabled.'); + + return 1; + } + + if ($vault->remove($name = $input->getArgument('name'))) { + $io->success($vault->getLastMessage() ?? 'Secret was removed from the vault.'); + } else { + $io->comment($vault->getLastMessage() ?? 'Secret was not found in the vault.'); + } + + if ($this->vault === $vault && null !== $this->localVault->reveal($name)) { + $io->comment('Note that this secret is overridden in the local vault.'); + } + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php new file mode 100644 index 0000000000000..ad7559d3f7ce3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Command/SecretsSetCommand.php @@ -0,0 +1,139 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Command; + +use Symfony\Bundle\FrameworkBundle\Secrets\AbstractVault; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Output\ConsoleOutputInterface; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; + +/** + * @author Tobias Schultze + * @author Jérémy Derussé + * @author Nicolas Grekas + * + * @internal + */ +final class SecretsSetCommand extends Command +{ + protected static $defaultName = 'secrets:set'; + + private $vault; + private $localVault; + + public function __construct(AbstractVault $vault, AbstractVault $localVault = null) + { + $this->vault = $vault; + $this->localVault = $localVault; + + parent::__construct(); + } + + protected function configure() + { + $this + ->setDescription('Set a secret in the vault.') + ->addArgument('name', InputArgument::REQUIRED, 'The name of the secret') + ->addArgument('file', InputArgument::OPTIONAL, 'A file where to read the secret from or "-" for reading from STDIN') + ->addOption('local', 'l', InputOption::VALUE_NONE, 'Update the local vault.') + ->addOption('random', 'r', InputOption::VALUE_OPTIONAL, 'Generate a random value.', false) + ->setHelp(<<<'EOF' +The %command.name% command stores a secret in the vault. + + %command.full_name% + +To reference secrets in services.yaml or any other config +files, use "%env()%". + +By default, the secret value should be entered interactively. +Alternatively, provide a file where to read the secret from: + + php %command.full_name% filename + +Use "-" as a file name to read from STDIN: + + cat filename | php %command.full_name% - + +Use --local to override secrets for local needs. +EOF + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output): int + { + $errOutput = $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output; + $io = new SymfonyStyle($input, $errOutput); + $name = $input->getArgument('name'); + $vault = $input->getOption('local') ? $this->localVault : $this->vault; + + if (null === $vault) { + $io->error('The local vault is disabled.'); + + return 1; + } + + if ($this->localVault === $vault && !\array_key_exists($name, $this->vault->list())) { + $io->error(sprintf('Secret "%s" does not exist in the vault, you cannot override it locally.', $name)); + + return 1; + } + + if (0 < $random = $input->getOption('random') ?? 16) { + $value = strtr(substr(base64_encode(random_bytes($random)), 0, $random), '+/', '-_'); + } elseif (!$file = $input->getArgument('file')) { + $value = $io->askHidden('Please type the secret value'); + + if (null === $value) { + $io->warning('No value provided: using empty string'); + $value = ''; + } + } elseif ('-' === $file) { + $value = file_get_contents('php://stdin'); + } elseif (is_file($file) && is_readable($file)) { + $value = file_get_contents($file); + } elseif (!is_file($file)) { + throw new \InvalidArgumentException(sprintf('File not found: "%s".', $file)); + } elseif (!is_readable($file)) { + throw new \InvalidArgumentException(sprintf('File is not readable: "%s".', $file)); + } + + if ($vault->generateKeys()) { + $io->success($vault->getLastMessage()); + + if ($this->vault === $vault) { + $io->caution('DO NOT COMMIT THE DECRYPTION KEY FOR THE PROD ENVIRONMENT⚠️'); + } + } + + $vault->seal($name, $value); + + $io->success($vault->getLastMessage() ?? 'Secret was successfully stored in the vault.'); + + if (0 < $random) { + $errOutput->write(' // The generated random value is: '); + $output->write($value); + $errOutput->writeln(''); + $io->newLine(); + } + + if ($this->vault === $vault && null !== $this->localVault->reveal($name)) { + $io->comment('Note that this secret is overridden in the local vault.'); + } + + return 0; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php index 86280c1cc875a..984c72e59f795 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationDebugCommand.php @@ -39,9 +39,9 @@ */ class TranslationDebugCommand extends Command { - const MESSAGE_MISSING = 0; - const MESSAGE_UNUSED = 1; - const MESSAGE_EQUALS_FALLBACK = 2; + public const MESSAGE_MISSING = 0; + public const MESSAGE_UNUSED = 1; + public const MESSAGE_EQUALS_FALLBACK = 2; protected static $defaultName = 'debug:translation'; @@ -59,7 +59,7 @@ class TranslationDebugCommand extends Command public function __construct($translator, TranslationReaderInterface $reader, ExtractorInterface $extractor, string $defaultTransPath = null, string $defaultViewsPath = null, array $transPaths = [], array $viewsPaths = []) { if (!$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); } parent::__construct(); @@ -82,11 +82,11 @@ protected function configure() new InputArgument('locale', InputArgument::REQUIRED, 'The locale'), new InputArgument('bundle', InputArgument::OPTIONAL, 'The bundle name or directory where to load the messages'), new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'The messages domain'), - new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Displays only missing messages'), - new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Displays only unused messages'), + new InputOption('only-missing', null, InputOption::VALUE_NONE, 'Display only missing messages'), + new InputOption('only-unused', null, InputOption::VALUE_NONE, 'Display only unused messages'), new InputOption('all', null, InputOption::VALUE_NONE, 'Load messages from all registered bundles'), ]) - ->setDescription('Displays translation messages information') + ->setDescription('Display translation messages information') ->setHelp(<<<'EOF' The %command.name% command helps finding unused or missing translation messages and comparing them with the fallback ones by inspecting the @@ -124,7 +124,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); @@ -139,7 +139,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (is_dir($dir = $rootDir.'/Resources/translations')) { if ($dir !== $this->defaultTransPath) { $notice = sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); } $transPaths[] = $dir; } @@ -150,7 +150,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (is_dir($dir = $rootDir.'/Resources/views')) { if ($dir !== $this->defaultViewsPath) { $notice = sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); } $viewsPaths[] = $dir; } @@ -162,23 +162,24 @@ protected function execute(InputInterface $input, OutputInterface $output) if (null !== $input->getArgument('bundle')) { try { $bundle = $kernel->getBundle($input->getArgument('bundle')); - $transPaths = [$bundle->getPath().'/Resources/translations']; + $bundleDir = $bundle->getPath(); + $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; + $viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; if ($this->defaultTransPath) { $transPaths[] = $this->defaultTransPath; } if (is_dir($dir = sprintf('%s/Resources/%s/translations', $rootDir, $bundle->getName()))) { $transPaths[] = $dir; $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $dir, $bundle->getName()); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); } - $viewsPaths = [$bundle->getPath().'/Resources/views']; if ($this->defaultViewsPath) { $viewsPaths[] = $this->defaultViewsPath; } if (is_dir($dir = sprintf('%s/Resources/%s/views', $rootDir, $bundle->getName()))) { $viewsPaths[] = $dir; $notice = sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); } } catch (\InvalidArgumentException $e) { // such a bundle does not exist, so treat the argument as path @@ -187,7 +188,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $transPaths = [$path.'/translations']; if (is_dir($dir = $path.'/Resources/translations')) { if ($dir !== $this->defaultTransPath) { - @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), E_USER_DEPRECATED); + @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), \E_USER_DEPRECATED); } $transPaths[] = $dir; } @@ -195,7 +196,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $viewsPaths = [$path.'/templates']; if (is_dir($dir = $path.'/Resources/views')) { if ($dir !== $this->defaultViewsPath) { - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), E_USER_DEPRECATED); + @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), \E_USER_DEPRECATED); } $viewsPaths[] = $dir; } @@ -206,17 +207,18 @@ protected function execute(InputInterface $input, OutputInterface $output) } } elseif ($input->getOption('all')) { foreach ($kernel->getBundles() as $bundle) { - $transPaths[] = $bundle->getPath().'/Resources/translations'; + $bundleDir = $bundle->getPath(); + $transPaths[] = is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundle->getPath().'/translations'; + $viewsPaths[] = is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundle->getPath().'/templates'; if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/translations', $rootDir, $bundle->getName()))) { $transPaths[] = $deprecatedPath; $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); } - $viewsPaths[] = $bundle->getPath().'/Resources/views'; if (is_dir($deprecatedPath = sprintf('%s/Resources/%s/views', $rootDir, $bundle->getName()))) { $viewsPaths[] = $deprecatedPath; $notice = sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, ', $bundle->getName(), $deprecatedPath); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); } } } @@ -244,7 +246,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $io->getErrorStyle()->warning($outputMessage); - return; + return 0; } // Load the fallback catalogues @@ -293,9 +295,11 @@ protected function execute(InputInterface $input, OutputInterface $output) } $io->table($headers, $rows); + + return 0; } - private function formatState($state): string + private function formatState(int $state): string { if (self::MESSAGE_MISSING === $state) { return ' missing '; diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php index ded71fd60e4bb..1004ae7899dc6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php @@ -23,6 +23,7 @@ use Symfony\Component\Translation\Catalogue\TargetOperation; use Symfony\Component\Translation\Extractor\ExtractorInterface; use Symfony\Component\Translation\MessageCatalogue; +use Symfony\Component\Translation\MessageCatalogueInterface; use Symfony\Component\Translation\Reader\TranslationReaderInterface; use Symfony\Component\Translation\Writer\TranslationWriterInterface; @@ -36,6 +37,10 @@ */ class TranslationUpdateCommand extends Command { + private const ASC = 'asc'; + private const DESC = 'desc'; + private const SORT_ORDERS = [self::ASC, self::DESC]; + protected static $defaultName = 'translation:update'; private $writer; @@ -78,22 +83,31 @@ protected function configure() new InputOption('clean', null, InputOption::VALUE_NONE, 'Should clean not found messages'), new InputOption('domain', null, InputOption::VALUE_OPTIONAL, 'Specify the domain to update'), new InputOption('xliff-version', null, InputOption::VALUE_OPTIONAL, 'Override the default xliff version', '1.2'), + new InputOption('sort', null, InputOption::VALUE_OPTIONAL, 'Return list of messages sorted alphabetically', 'asc'), ]) - ->setDescription('Updates the translation file') + ->setDescription('Update the translation file') ->setHelp(<<<'EOF' The %command.name% command extracts translation strings from templates -of a given bundle or the default translations directory. It can display them or merge the new ones into the translation files. +of a given bundle or the default translations directory. It can display them or merge +the new ones into the translation files. When new translation strings are found it can automatically add a prefix to the translation message. Example running against a Bundle (AcmeBundle) + php %command.full_name% --dump-messages en AcmeBundle php %command.full_name% --force --prefix="new_" fr AcmeBundle Example running against default messages directory + php %command.full_name% --dump-messages en php %command.full_name% --force --prefix="new_" fr + +You can sort the output with the --sort flag: + + php %command.full_name% --dump-messages --sort=asc en AcmeBundle + php %command.full_name% --dump-messages --sort=desc fr EOF ) ; @@ -102,7 +116,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $errorIo = $io->getErrorStyle(); @@ -130,7 +144,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (is_dir($dir = $rootDir.'/Resources/translations')) { if ($dir !== $this->defaultTransPath) { $notice = sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); } $transPaths[] = $dir; } @@ -141,7 +155,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if (is_dir($dir = $rootDir.'/Resources/views')) { if ($dir !== $this->defaultViewsPath) { $notice = sprintf('Storing templates in the "%s" directory is deprecated since Symfony 4.2, ', $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); } $viewsPaths[] = $dir; } @@ -154,23 +168,24 @@ protected function execute(InputInterface $input, OutputInterface $output) if (null !== $input->getArgument('bundle')) { try { $foundBundle = $kernel->getBundle($input->getArgument('bundle')); - $transPaths = [$foundBundle->getPath().'/Resources/translations']; + $bundleDir = $foundBundle->getPath(); + $transPaths = [is_dir($bundleDir.'/Resources/translations') ? $bundleDir.'/Resources/translations' : $bundleDir.'/translations']; + $viewsPaths = [is_dir($bundleDir.'/Resources/views') ? $bundleDir.'/Resources/views' : $bundleDir.'/templates']; if ($this->defaultTransPath) { $transPaths[] = $this->defaultTransPath; } if (is_dir($dir = sprintf('%s/Resources/%s/translations', $rootDir, $foundBundle->getName()))) { $transPaths[] = $dir; $notice = sprintf('Storing translations files for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $foundBundle->getName(), $dir); - @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultTransPath ? sprintf('use the "%s" directory instead.', $this->defaultTransPath) : 'configure and use "framework.translator.default_path" instead.'), \E_USER_DEPRECATED); } - $viewsPaths = [$foundBundle->getPath().'/Resources/views']; if ($this->defaultViewsPath) { $viewsPaths[] = $this->defaultViewsPath; } if (is_dir($dir = sprintf('%s/Resources/%s/views', $rootDir, $foundBundle->getName()))) { $viewsPaths[] = $dir; $notice = sprintf('Storing templates for "%s" in the "%s" directory is deprecated since Symfony 4.2, ', $foundBundle->getName(), $dir); - @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), E_USER_DEPRECATED); + @trigger_error($notice.($this->defaultViewsPath ? sprintf('use the "%s" directory instead.', $this->defaultViewsPath) : 'configure and use "twig.default_path" instead.'), \E_USER_DEPRECATED); } $currentName = $foundBundle->getName(); } catch (\InvalidArgumentException $e) { @@ -180,7 +195,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $transPaths = [$path.'/translations']; if (is_dir($dir = $path.'/Resources/translations')) { if ($dir !== $this->defaultTransPath) { - @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), E_USER_DEPRECATED); + @trigger_error(sprintf('Storing translations in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/translations'), \E_USER_DEPRECATED); } $transPaths[] = $dir; } @@ -188,7 +203,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $viewsPaths = [$path.'/templates']; if (is_dir($dir = $path.'/Resources/views')) { if ($dir !== $this->defaultViewsPath) { - @trigger_error(sprintf('Storing templates in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), E_USER_DEPRECATED); + @trigger_error(sprintf('Storing templates in the "%s" directory is deprecated since Symfony 4.2, use the "%s" directory instead.', $dir, $path.'/templates'), \E_USER_DEPRECATED); } $viewsPaths[] = $dir; } @@ -199,12 +214,12 @@ protected function execute(InputInterface $input, OutputInterface $output) } } - $errorIo->title('Translation Messages Extractor and Dumper'); - $errorIo->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); + $io->title('Translation Messages Extractor and Dumper'); + $io->comment(sprintf('Generating "%s" translation files for "%s"', $input->getArgument('locale'), $currentName)); // load any messages from templates $extractedCatalogue = new MessageCatalogue($input->getArgument('locale')); - $errorIo->comment('Parsing templates...'); + $io->comment('Parsing templates...'); $this->extractor->setPrefix($input->getOption('prefix')); foreach ($viewsPaths as $path) { if (is_dir($path) || is_file($path)) { @@ -214,7 +229,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // load any existing messages from the translation files $currentCatalogue = new MessageCatalogue($input->getArgument('locale')); - $errorIo->comment('Loading translation files...'); + $io->comment('Loading translation files...'); foreach ($transPaths as $path) { if (is_dir($path)) { $this->reader->read($path, $currentCatalogue); @@ -235,11 +250,29 @@ protected function execute(InputInterface $input, OutputInterface $output) if (!\count($operation->getDomains())) { $errorIo->warning('No translation messages were found.'); - return; + return 0; } $resultMessage = 'Translation files were successfully updated'; + // move new messages to intl domain when possible + if (class_exists(\MessageFormatter::class)) { + foreach ($operation->getDomains() as $domain) { + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + $newMessages = $operation->getNewMessages($domain); + + if ([] === $newMessages || ([] === $currentCatalogue->all($intlDomain) && [] !== $currentCatalogue->all($domain))) { + continue; + } + + $result = $operation->getResult(); + $allIntlMessages = $result->all($intlDomain); + $currentMessages = array_diff_key($newMessages, $result->all($domain)); + $result->replace($currentMessages, $domain); + $result->replace($allIntlMessages + $newMessages, $intlDomain); + } + } + // show compiled list of messages if (true === $input->getOption('dump-messages')) { $extractedMessagesCount = 0; @@ -260,6 +293,21 @@ protected function execute(InputInterface $input, OutputInterface $output) $domainMessagesCount = \count($list); + if ($sort = $input->getOption('sort')) { + $sort = strtolower($sort); + if (!\in_array($sort, self::SORT_ORDERS, true)) { + $errorIo->error(['Wrong sort order', 'Supported formats are: '.implode(', ', self::SORT_ORDERS).'.']); + + return 1; + } + + if (self::DESC === $sort) { + rsort($list); + } else { + sort($list); + } + } + $io->section(sprintf('Messages extracted for domain "%s" (%d message%s)', $domain, $domainMessagesCount, $domainMessagesCount > 1 ? 's' : '')); $io->listing($list); @@ -267,7 +315,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } if ('xlf' === $input->getOption('output-format')) { - $errorIo->comment(sprintf('Xliff output version is %s', $input->getOption('xliff-version'))); + $io->comment(sprintf('Xliff output version is %s', $input->getOption('xliff-version'))); } $resultMessage = sprintf('%d message%s successfully extracted', $extractedMessagesCount, $extractedMessagesCount > 1 ? 's were' : ' was'); @@ -279,7 +327,7 @@ protected function execute(InputInterface $input, OutputInterface $output) // save the files if (true === $input->getOption('force')) { - $errorIo->comment('Writing files...'); + $io->comment('Writing files...'); $bundleTransPath = false; foreach ($transPaths as $path) { @@ -299,19 +347,33 @@ protected function execute(InputInterface $input, OutputInterface $output) } } - $errorIo->success($resultMessage.'.'); + $io->success($resultMessage.'.'); + + return 0; } private function filterCatalogue(MessageCatalogue $catalogue, string $domain): MessageCatalogue { $filteredCatalogue = new MessageCatalogue($catalogue->getLocale()); - if ($messages = $catalogue->all($domain)) { + // extract intl-icu messages only + $intlDomain = $domain.MessageCatalogueInterface::INTL_DOMAIN_SUFFIX; + if ($intlMessages = $catalogue->all($intlDomain)) { + $filteredCatalogue->add($intlMessages, $intlDomain); + } + + // extract all messages and subtract intl-icu messages + if ($messages = array_diff($catalogue->all($domain), $intlMessages)) { $filteredCatalogue->add($messages, $domain); } foreach ($catalogue->getResources() as $resource) { $filteredCatalogue->addResource($resource); } + if ($metadata = $catalogue->getMetadata('', $intlDomain)) { + foreach ($metadata as $k => $v) { + $filteredCatalogue->setMetadata($k, $v, $intlDomain); + } + } if ($metadata = $catalogue->getMetadata('', $domain)) { foreach ($metadata as $k => $v) { $filteredCatalogue->setMetadata($k, $v, $domain); diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php index ff91dc781837c..ecdca7cb39452 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/WorkflowDumpCommand.php @@ -40,7 +40,7 @@ protected function configure() ->setDefinition([ new InputArgument('name', InputArgument::REQUIRED, 'A workflow name'), new InputArgument('marking', InputArgument::IS_ARRAY, 'A marking (a list of places)'), - new InputOption('label', 'l', InputOption::VALUE_REQUIRED, 'Labels a graph'), + new InputOption('label', 'l', InputOption::VALUE_REQUIRED, 'Label a graph'), new InputOption('dump-format', null, InputOption::VALUE_REQUIRED, 'The dump format [dot|puml]', 'dot'), ]) ->setDescription('Dump a workflow') @@ -59,7 +59,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $container = $this->getApplication()->getKernel()->getContainer(); $serviceId = $input->getArgument('name'); @@ -97,5 +97,7 @@ protected function execute(InputInterface $input, OutputInterface $output) ], ]; $output->writeln($dumper->dump($workflow->getDefinition(), $marking, $options)); + + return 0; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php index 0b5bb061d66e2..648e210d4807d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/XliffLintCommand.php @@ -37,7 +37,7 @@ public function __construct() }; $isReadableProvider = function ($fileOrDirectory, $default) { - return 0 === strpos($fileOrDirectory, '@') || $default($fileOrDirectory); + return str_starts_with($fileOrDirectory, '@') || $default($fileOrDirectory); }; parent::__construct(null, $directoryIteratorProvider, $isReadableProvider); @@ -57,6 +57,6 @@ protected function configure() php %command.full_name% @AcmeDemoBundle EOF - ); + ); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php index 1163ff1c28fb1..86787361aa274 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php +++ b/src/Symfony/Bundle/FrameworkBundle/Command/YamlLintCommand.php @@ -36,7 +36,7 @@ public function __construct() }; $isReadableProvider = function ($fileOrDirectory, $default) { - return 0 === strpos($fileOrDirectory, '@') || $default($fileOrDirectory); + return str_starts_with($fileOrDirectory, '@') || $default($fileOrDirectory); }; parent::__construct(null, $directoryIteratorProvider, $isReadableProvider); diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php index 0fdb7ecd44abf..20fb6e05f73de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Application.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Application.php @@ -19,8 +19,8 @@ use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Debug\Exception\FatalThrowableError; use Symfony\Component\DependencyInjection\ContainerAwareInterface; -use Symfony\Component\ErrorHandler\Exception\FatalThrowableError; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelInterface; @@ -148,7 +148,7 @@ public function all($namespace = null) */ public function getLongVersion() { - return parent::getLongVersion().sprintf(' (env: %s, debug: %s)', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); + return parent::getLongVersion().sprintf(' (env: %s, debug: %s) #StandWithUkraine https://sf.to/ukraine', $this->kernel->getEnvironment(), $this->kernel->isDebug() ? 'true' : 'false'); } public function add(Command $command) @@ -174,10 +174,8 @@ protected function registerCommands() if ($bundle instanceof Bundle) { try { $bundle->registerCommands($this); - } catch (\Exception $e) { - $this->registrationErrors[] = $e; } catch (\Throwable $e) { - $this->registrationErrors[] = new FatalThrowableError($e); + $this->registrationErrors[] = $e; } } } @@ -192,10 +190,8 @@ protected function registerCommands() if (!isset($lazyCommandIds[$id])) { try { $this->add($container->get($id)); - } catch (\Exception $e) { - $this->registrationErrors[] = $e; } catch (\Throwable $e) { - $this->registrationErrors[] = new FatalThrowableError($e); + $this->registrationErrors[] = $e; } } } @@ -211,7 +207,15 @@ private function renderRegistrationErrors(InputInterface $input, OutputInterface (new SymfonyStyle($input, $output))->warning('Some commands could not be registered:'); foreach ($this->registrationErrors as $error) { - $this->doRenderException($error, $output); + if (method_exists($this, 'doRenderThrowable')) { + $this->doRenderThrowable($error, $output); + } else { + if (!$error instanceof \Exception) { + $error = new FatalThrowableError($error); + } + + $this->doRenderException($error, $output); + } } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php index e454633c787d9..5e90f7ba9f8d0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/Descriptor.php @@ -84,45 +84,22 @@ public function describe(OutputInterface $output, $object, array $options = []) } } - /** - * Returns the output. - * - * @return OutputInterface The output - */ - protected function getOutput() + protected function getOutput(): OutputInterface { return $this->output; } - /** - * Writes content to output. - * - * @param string $content - * @param bool $decorated - */ - protected function write($content, $decorated = false) + protected function write(string $content, bool $decorated = false) { $this->output->write($content, false, $decorated ? OutputInterface::OUTPUT_NORMAL : OutputInterface::OUTPUT_RAW); } - /** - * Describes an InputArgument instance. - */ abstract protected function describeRouteCollection(RouteCollection $routes, array $options = []); - /** - * Describes an InputOption instance. - */ abstract protected function describeRoute(Route $route, array $options = []); - /** - * Describes container parameters. - */ abstract protected function describeContainerParameters(ParameterBag $parameters, array $options = []); - /** - * Describes container tags. - */ abstract protected function describeContainerTags(ContainerBuilder $builder, array $options = []); /** @@ -132,8 +109,6 @@ abstract protected function describeContainerTags(ContainerBuilder $builder, arr * * name: name of described service * * @param Definition|Alias|object $service - * @param array $options - * @param ContainerBuilder|null $builder */ abstract protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null); @@ -145,24 +120,12 @@ abstract protected function describeContainerService($service, array $options = */ abstract protected function describeContainerServices(ContainerBuilder $builder, array $options = []); - /** - * Describes a service definition. - */ abstract protected function describeContainerDefinition(Definition $definition, array $options = []); - /** - * Describes a service alias. - */ abstract protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null); - /** - * Describes a container parameter. - */ abstract protected function describeContainerParameter($parameter, array $options = []); - /** - * Describes container environment variables. - */ abstract protected function describeContainerEnvVars(array $envs, array $options = []); /** @@ -176,8 +139,7 @@ abstract protected function describeEventDispatcherListeners(EventDispatcherInte /** * Describes a callable. * - * @param callable $callable - * @param array $options + * @param mixed $callable */ abstract protected function describeCallable($callable, array $options = []); @@ -185,11 +147,13 @@ abstract protected function describeCallable($callable, array $options = []); * Formats a value as string. * * @param mixed $value - * - * @return string */ - protected function formatValue($value) + protected function formatValue($value): string { + if ($value instanceof \UnitEnum) { + return ltrim(var_export($value, true), '\\'); + } + if (\is_object($value)) { return sprintf('object(%s)', \get_class($value)); } @@ -205,11 +169,23 @@ protected function formatValue($value) * Formats a parameter. * * @param mixed $value - * - * @return string */ - protected function formatParameter($value) + protected function formatParameter($value): string { + if ($value instanceof \UnitEnum) { + return ltrim(var_export($value, true), '\\'); + } + + // Recursively search for enum values, so we can replace it + // before json_encode (which will not display anything for \UnitEnum otherwise) + if (\is_array($value)) { + array_walk_recursive($value, static function (&$value) { + if ($value instanceof \UnitEnum) { + $value = ltrim(var_export($value, true), '\\'); + } + }); + } + if (\is_bool($value) || \is_array($value) || (null === $value)) { $jsonString = json_encode($value); @@ -224,12 +200,9 @@ protected function formatParameter($value) } /** - * @param ContainerBuilder $builder - * @param string $serviceId - * * @return mixed */ - protected function resolveServiceDefinition(ContainerBuilder $builder, $serviceId) + protected function resolveServiceDefinition(ContainerBuilder $builder, string $serviceId) { if ($builder->hasDefinition($serviceId)) { return $builder->getDefinition($serviceId); @@ -248,13 +221,7 @@ protected function resolveServiceDefinition(ContainerBuilder $builder, $serviceI return $builder->get($serviceId); } - /** - * @param ContainerBuilder $builder - * @param bool $showHidden - * - * @return array - */ - protected function findDefinitionsByTag(ContainerBuilder $builder, $showHidden) + protected function findDefinitionsByTag(ContainerBuilder $builder, bool $showHidden): array { $definitions = []; $tags = $builder->findTags(); @@ -294,9 +261,44 @@ protected function sortServiceIds(array $serviceIds) return $serviceIds; } - /** - * Gets class description from a docblock. - */ + protected function sortTaggedServicesByPriority(array $services): array + { + $maxPriority = []; + foreach ($services as $service => $tags) { + $maxPriority[$service] = \PHP_INT_MIN; + foreach ($tags as $tag) { + $currentPriority = $tag['priority'] ?? 0; + if ($maxPriority[$service] < $currentPriority) { + $maxPriority[$service] = $currentPriority; + } + } + } + uasort($maxPriority, function ($a, $b) { + return $b <=> $a; + }); + + return array_keys($maxPriority); + } + + protected function sortTagsByPriority(array $tags): array + { + $sortedTags = []; + foreach ($tags as $tagName => $tag) { + $sortedTags[$tagName] = $this->sortByPriority($tag); + } + + return $sortedTags; + } + + protected function sortByPriority(array $tag): array + { + usort($tag, function ($a, $b) { + return ($b['priority'] ?? 0) <=> ($a['priority'] ?? 0); + }); + + return $tag; + } + public static function getClassDescription(string $class, string &$resolvedClass = null): string { $resolvedClass = $class; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php index 0665b34dfbd3a..3af3ec03f437a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/JsonDescriptor.php @@ -30,9 +30,6 @@ */ class JsonDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ protected function describeRouteCollection(RouteCollection $routes, array $options = []) { $data = []; @@ -43,25 +40,16 @@ protected function describeRouteCollection(RouteCollection $routes, array $optio $this->writeData($data, $options); } - /** - * {@inheritdoc} - */ protected function describeRoute(Route $route, array $options = []) { $this->writeData($this->getRouteData($route), $options); } - /** - * {@inheritdoc} - */ protected function describeContainerParameters(ParameterBag $parameters, array $options = []) { $this->writeData($this->sortParameters($parameters), $options); } - /** - * {@inheritdoc} - */ protected function describeContainerTags(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; @@ -77,9 +65,6 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio $this->writeData($data, $options); } - /** - * {@inheritdoc} - */ protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { @@ -95,12 +80,11 @@ protected function describeContainerService($service, array $options = [], Conta } } - /** - * {@inheritdoc} - */ protected function describeContainerServices(ContainerBuilder $builder, array $options = []) { - $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $serviceIds = isset($options['tag']) && $options['tag'] + ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag'])) + : $this->sortServiceIds($builder->getServiceIds()); $showHidden = isset($options['show_hidden']) && $options['show_hidden']; $omitTags = isset($options['omit_tags']) && $options['omit_tags']; $showArguments = isset($options['show_arguments']) && $options['show_arguments']; @@ -110,7 +94,7 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $serviceIds = array_filter($serviceIds, $options['filter']); } - foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + foreach ($serviceIds as $serviceId) { $service = $this->resolveServiceDefinition($builder, $serviceId); if ($showHidden xor '.' === ($serviceId[0] ?? null)) { @@ -129,21 +113,17 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $this->writeData($data, $options); } - /** - * {@inheritdoc} - */ protected function describeContainerDefinition(Definition $definition, array $options = []) { $this->writeData($this->getContainerDefinitionData($definition, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments']), $options); } - /** - * {@inheritdoc} - */ protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null) { if (!$builder) { - return $this->writeData($this->getContainerAliasData($alias), $options); + $this->writeData($this->getContainerAliasData($alias), $options); + + return; } $this->writeData( @@ -152,56 +132,44 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con ); } - /** - * {@inheritdoc} - */ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { - $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, \array_key_exists('event', $options) ? $options['event'] : null), $options); + $this->writeData($this->getEventDispatcherListenersData($eventDispatcher, $options['event'] ?? null), $options); } - /** - * {@inheritdoc} - */ protected function describeCallable($callable, array $options = []) { - $this->writeData($this->getCallableData($callable, $options), $options); + $this->writeData($this->getCallableData($callable), $options); } - /** - * {@inheritdoc} - */ protected function describeContainerParameter($parameter, array $options = []) { - $key = isset($options['parameter']) ? $options['parameter'] : ''; + $key = $options['parameter'] ?? ''; $this->writeData([$key => $parameter], $options); } - /** - * {@inheritdoc} - */ protected function describeContainerEnvVars(array $envs, array $options = []) { throw new LogicException('Using the JSON format to debug environment variables is not supported.'); } - /** - * Writes data as json. - * - * @return array|string - */ private function writeData(array $data, array $options) { - $flags = isset($options['json_encoding']) ? $options['json_encoding'] : 0; + $flags = $options['json_encoding'] ?? 0; + + // Recursively search for enum values, so we can replace it + // before json_encode (which will not display anything for \UnitEnum otherwise) + array_walk_recursive($data, static function (&$value) { + if ($value instanceof \UnitEnum) { + $value = ltrim(var_export($value, true), '\\'); + } + }); - $this->write(json_encode($data, $flags | JSON_PRETTY_PRINT)."\n"); + $this->write(json_encode($data, $flags | \JSON_PRETTY_PRINT)."\n"); } - /** - * @return array - */ - protected function getRouteData(Route $route) + protected function getRouteData(Route $route): array { $data = [ 'path' => $route->getPath(), @@ -251,7 +219,7 @@ private function getContainerDefinitionData(Definition $definition, bool $omitTa if ($factory[0] instanceof Reference) { $data['factory_service'] = (string) $factory[0]; } elseif ($factory[0] instanceof Definition) { - throw new \InvalidArgumentException('Factory is not describable.'); + $data['factory_service'] = sprintf('inline factory service (%s)', $factory[0]->getClass() ?? 'class not configured'); } else { $data['factory_class'] = $factory[0]; } @@ -271,7 +239,7 @@ private function getContainerDefinitionData(Definition $definition, bool $omitTa if (!$omitTags) { $data['tags'] = []; - foreach ($definition->getTags() as $tagName => $tagData) { + foreach ($this->sortTagsByPriority($definition->getTags()) as $tagName => $tagData) { foreach ($tagData as $parameters) { $data['tags'][] = ['name' => $tagName, 'parameters' => $parameters]; } @@ -315,7 +283,7 @@ private function getEventDispatcherListenersData(EventDispatcherInterface $event return $data; } - private function getCallableData($callable, array $options = []): array + private function getCallableData($callable): array { $data = []; @@ -326,7 +294,7 @@ private function getCallableData($callable, array $options = []): array $data['name'] = $callable[1]; $data['class'] = \get_class($callable[0]); } else { - if (0 !== strpos($callable[1], 'parent::')) { + if (!str_starts_with($callable[1], 'parent::')) { $data['name'] = $callable[1]; $data['class'] = $callable[0]; $data['static'] = true; @@ -344,7 +312,7 @@ private function getCallableData($callable, array $options = []): array if (\is_string($callable)) { $data['type'] = 'function'; - if (false === strpos($callable, '::')) { + if (!str_contains($callable, '::')) { $data['name'] = $callable; } else { $callableParts = explode('::', $callable); @@ -361,7 +329,7 @@ private function getCallableData($callable, array $options = []): array $data['type'] = 'closure'; $r = new \ReflectionFunction($callable); - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return $data; } $data['name'] = $r->name; @@ -386,7 +354,7 @@ private function getCallableData($callable, array $options = []): array throw new \InvalidArgumentException('Callable is not describable.'); } - private function describeValue($value, $omitTags, $showArguments) + private function describeValue($value, bool $omitTags, bool $showArguments) { if (\is_array($value)) { $data = []; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php index 858eb79ad2aa3..4d48620236f79 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/MarkdownDescriptor.php @@ -28,9 +28,6 @@ */ class MarkdownDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ protected function describeRouteCollection(RouteCollection $routes, array $options = []) { $first = true; @@ -45,9 +42,6 @@ protected function describeRouteCollection(RouteCollection $routes, array $optio $this->write("\n"); } - /** - * {@inheritdoc} - */ protected function describeRoute(Route $route, array $options = []) { $output = '- Path: '.$route->getPath() @@ -71,9 +65,6 @@ protected function describeRoute(Route $route, array $options = []) $this->write("\n"); } - /** - * {@inheritdoc} - */ protected function describeContainerParameters(ParameterBag $parameters, array $options = []) { $this->write("Container parameters\n====================\n"); @@ -82,9 +73,6 @@ protected function describeContainerParameters(ParameterBag $parameters, array $ } } - /** - * {@inheritdoc} - */ protected function describeContainerTags(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; @@ -99,9 +87,6 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio } } - /** - * {@inheritdoc} - */ protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { @@ -119,9 +104,6 @@ protected function describeContainerService($service, array $options = [], Conta } } - /** - * {@inheritdoc} - */ protected function describeContainerServices(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; @@ -132,7 +114,9 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o } $this->write($title."\n".str_repeat('=', \strlen($title))); - $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $serviceIds = isset($options['tag']) && $options['tag'] + ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag'])) + : $this->sortServiceIds($builder->getServiceIds()); $showArguments = isset($options['show_arguments']) && $options['show_arguments']; $services = ['definitions' => [], 'aliases' => [], 'services' => []]; @@ -140,7 +124,7 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $serviceIds = array_filter($serviceIds, $options['filter']); } - foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + foreach ($serviceIds as $serviceId) { $service = $this->resolveServiceDefinition($builder, $serviceId); if ($showHidden xor '.' === ($serviceId[0] ?? null)) { @@ -181,9 +165,6 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o } } - /** - * {@inheritdoc} - */ protected function describeContainerDefinition(Definition $definition, array $options = []) { $output = ''; @@ -215,7 +196,7 @@ protected function describeContainerDefinition(Definition $definition, array $op if ($factory[0] instanceof Reference) { $output .= "\n".'- Factory Service: `'.$factory[0].'`'; } elseif ($factory[0] instanceof Definition) { - throw new \InvalidArgumentException('Factory is not describable.'); + $output .= "\n".sprintf('- Factory Service: inline factory service (%s)', $factory[0]->getClass() ? sprintf('`%s`', $factory[0]->getClass()) : 'not configured'); } else { $output .= "\n".'- Factory Class: `'.$factory[0].'`'; } @@ -231,7 +212,7 @@ protected function describeContainerDefinition(Definition $definition, array $op } if (!(isset($options['omit_tags']) && $options['omit_tags'])) { - foreach ($definition->getTags() as $tagName => $tagData) { + foreach ($this->sortTagsByPriority($definition->getTags()) as $tagName => $tagData) { foreach ($tagData as $parameters) { $output .= "\n".'- Tag: `'.$tagName.'`'; foreach ($parameters as $name => $value) { @@ -244,16 +225,15 @@ protected function describeContainerDefinition(Definition $definition, array $op $this->write(isset($options['id']) ? sprintf("### %s\n\n%s\n", $options['id'], $output) : $output); } - /** - * {@inheritdoc} - */ protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null) { $output = '- Service: `'.$alias.'`' ."\n".'- Public: '.($alias->isPublic() && !$alias->isPrivate() ? 'yes' : 'no'); if (!isset($options['id'])) { - return $this->write($output); + $this->write($output); + + return; } $this->write(sprintf("### %s\n\n%s\n", $options['id'], $output)); @@ -266,28 +246,19 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias])); } - /** - * {@inheritdoc} - */ protected function describeContainerParameter($parameter, array $options = []) { $this->write(isset($options['parameter']) ? sprintf("%s\n%s\n\n%s", $options['parameter'], str_repeat('=', \strlen($options['parameter'])), $this->formatParameter($parameter)) : $parameter); } - /** - * {@inheritdoc} - */ protected function describeContainerEnvVars(array $envs, array $options = []) { throw new LogicException('Using the markdown format to debug environment variables is not supported.'); } - /** - * {@inheritdoc} - */ protected function describeEventDispatcherListeners(EventDispatcherInterface $ev 10000 entDispatcher, array $options = []) { - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; $title = 'Registered listeners'; if (null !== $event) { @@ -318,9 +289,6 @@ protected function describeEventDispatcherListeners(EventDispatcherInterface $ev } } - /** - * {@inheritdoc} - */ protected function describeCallable($callable, array $options = []) { $string = ''; @@ -332,7 +300,7 @@ protected function describeCallable($callable, array $options = []) $string .= "\n".sprintf('- Name: `%s`', $callable[1]); $string .= "\n".sprintf('- Class: `%s`', \get_class($callable[0])); } else { - if (0 !== strpos($callable[1], 'parent::')) { + if (!str_starts_with($callable[1], 'parent::')) { $string .= "\n".sprintf('- Name: `%s`', $callable[1]); $string .= "\n".sprintf('- Class: `%s`', $callable[0]); $string .= "\n- Static: yes"; @@ -350,7 +318,7 @@ protected function describeCallable($callable, array $options = []) if (\is_string($callable)) { $string .= "\n- Type: `function`"; - if (false === strpos($callable, '::')) { + if (!str_contains($callable, '::')) { $string .= "\n".sprintf('- Name: `%s`', $callable); } else { $callableParts = explode('::', $callable); @@ -367,7 +335,7 @@ protected function describeCallable($callable, array $options = []) $string .= "\n- Type: `closure`"; $r = new \ReflectionFunction($callable); - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return $this->write($string."\n"); } $string .= "\n".sprintf('- Name: `%s`', $r->name); @@ -392,10 +360,7 @@ protected function describeCallable($callable, array $options = []) throw new \InvalidArgumentException('Callable is not describable.'); } - /** - * @return string - */ - private function formatRouterConfig(array $array) + private function formatRouterConfig(array $array): string { if (!$array) { return 'NONE'; diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php index 18b13a215c1e1..5163a8730795b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/TextDescriptor.php @@ -43,9 +43,6 @@ public function __construct(FileLinkFormatter $fileLinkFormatter = null) $this->fileLinkFormatter = $fileLinkFormatter; } - /** - * {@inheritdoc} - */ protected function describeRouteCollection(RouteCollection $routes, array $options = []) { $showControllers = isset($options['show_controllers']) && $options['show_controllers']; @@ -83,21 +80,18 @@ protected function describeRouteCollection(RouteCollection $routes, array $optio } } - /** - * {@inheritdoc} - */ protected function describeRoute(Route $route, array $options = []) { $tableHeaders = ['Property', 'Value']; $tableRows = [ - ['Route Name', isset($options['name']) ? $options['name'] : ''], + ['Route Name', $options['name'] ?? ''], ['Path', $route->getPath()], ['Path Regex', $route->compile()->getRegex()], - ['Host', ('' !== $route->getHost() ? $route->getHost() : 'ANY')], - ['Host Regex', ('' !== $route->getHost() ? $route->compile()->getHostRegex() : '')], - ['Scheme', ($route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY')], - ['Method', ($route->getMethods() ? implode('|', $route->getMethods()) : 'ANY')], - ['Requirements', ($route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM')], + ['Host', '' !== $route->getHost() ? $route->getHost() : 'ANY'], + ['Host Regex', '' !== $route->getHost() ? $route->compile()->getHostRegex() : ''], + ['Scheme', $route->getSchemes() ? implode('|', $route->getSchemes()) : 'ANY'], + ['Method', $route->getMethods() ? implode('|', $route->getMethods()) : 'ANY'], + ['Requirements', $route->getRequirements() ? $this->formatRouterConfig($route->getRequirements()) : 'NO CUSTOM'], ['Class', \get_class($route)], ['Defaults', $this->formatRouterConfig($route->getDefaults())], ['Options', $this->formatRouterConfig($route->getOptions())], @@ -112,9 +106,6 @@ protected function describeRoute(Route $route, array $options = []) $table->render(); } - /** - * {@inheritdoc} - */ protected function describeContainerParameters(ParameterBag $parameters, array $options = []) { $tableHeaders = ['Parameter', 'Value']; @@ -128,9 +119,6 @@ protected function describeContainerParameters(ParameterBag $parameters, array $ $options['output']->table($tableHeaders, $tableRows); } - /** - * {@inheritdoc} - */ protected function describeContainerTags(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; @@ -147,9 +135,6 @@ protected function describeContainerTags(ContainerBuilder $builder, array $optio } } - /** - * {@inheritdoc} - */ protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { @@ -165,19 +150,16 @@ protected function describeContainerService($service, array $options = [], Conta $options['output']->table( ['Service ID', 'Class'], [ - [isset($options['id']) ? $options['id'] : '-', \get_class($service)], + [$options['id'] ?? '-', \get_class($service)], ] ); } } - /** - * {@inheritdoc} - */ protected function describeContainerServices(ContainerBuilder $builder, array $options = []) { $showHidden = isset($options['show_hidden']) && $options['show_hidden']; - $showTag = isset($options['tag']) ? $options['tag'] : null; + $showTag = $options['tag'] ?? null; if ($showHidden) { $title = 'Symfony Container Hidden Services'; @@ -191,7 +173,9 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $options['output']->title($title); - $serviceIds = isset($options['tag']) && $options['tag'] ? array_keys($builder->findTaggedServiceIds($options['tag'])) : $builder->getServiceIds(); + $serviceIds = isset($options['tag']) && $options['tag'] + ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($options['tag'])) + : $this->sortServiceIds($builder->getServiceIds()); $maxTags = []; if (isset($options['filter'])) { @@ -230,16 +214,16 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $tableHeaders = array_merge(['Service ID'], $tagsNames, ['Class name']); $tableRows = []; $rawOutput = isset($options['raw_text']) && $options['raw_text']; - foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + foreach ($serviceIds as $serviceId) { $definition = $this->resolveServiceDefinition($builder, $serviceId); $styledServiceId = $rawOutput ? $serviceId : sprintf('%s', OutputFormatter::escape($serviceId)); if ($definition instanceof Definition) { if ($showTag) { - foreach ($definition->getTag($showTag) as $key => $tag) { + foreach ($this->sortByPriority($definition->getTag($showTag)) as $key => $tag) { $tagValues = []; foreach ($tagsNames as $tagName) { - $tagValues[] = isset($tag[$tagName]) ? $tag[$tagName] : ''; + $tagValues[] = $tag[$tagName] ?? ''; } if (0 === $key) { $tableRows[] = array_merge([$serviceId], $tagValues, [$definition->getClass()]); @@ -261,9 +245,6 @@ protected function describeContainerServices(ContainerBuilder $builder, array $o $options['output']->table($tableHeaders, $tableRows); } - /** - * {@inheritdoc} - */ protected function describeContainerDefinition(Definition $definition, array $options = []) { if (isset($options['id'])) { @@ -276,7 +257,7 @@ protected function describeContainerDefinition(Definition $definition, array $op $tableHeaders = ['Option', 'Value']; - $tableRows[] = ['Service ID', isset($options['id']) ? $options['id'] : '-']; + $tableRows[] = ['Service ID', $options['id'] ?? '-']; $tableRows[] = ['Class', $definition->getClass() ?: '-']; $omitTags = isset($options['omit_tags']) && $options['omit_tags']; @@ -328,7 +309,7 @@ protected function describeContainerDefinition(Definition $definition, array $op if ($factory[0] instanceof Reference) { $tableRows[] = ['Factory Service', $factory[0]]; } elseif ($factory[0] instanceof Definition) { - throw new \InvalidArgumentException('Factory is not describable.'); + $tableRows[] = ['Factory Service', sprintf('inline factory service (%s)', $factory[0]->getClass() ?? 'class not configured')]; } else { $tableRows[] = ['Factory Class', $factory[0]]; } @@ -361,6 +342,8 @@ protected function describeContainerDefinition(Definition $definition, array $op $argumentsInformation[] = sprintf('Service locator (%d element(s))', \count($argument->getValues())); } elseif ($argument instanceof Definition) { $argumentsInformation[] = 'Inlined Service'; + } elseif ($argument instanceof \UnitEnum) { + $argumentsInformation[] = ltrim(var_export($argument, true), '\\'); } else { $argumentsInformation[] = \is_array($argument) ? sprintf('Array (%d element(s))', \count($argument)) : $argument; } @@ -372,12 +355,9 @@ protected function describeContainerDefinition(Definition $definition, array $op $options['output']->table($tableHeaders, $tableRows); } - /** - * {@inheritdoc} - */ protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null) { - if ($alias->isPublic()) { + if ($alias->isPublic() && !$alias->isPrivate()) { $options['output']->comment(sprintf('This service is a public alias for the service %s', (string) $alias)); } else { $options['output']->comment(sprintf('This service is a private alias for the service %s', (string) $alias)); @@ -387,12 +367,9 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con return; } - return $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias])); + $this->describeContainerDefinition($builder->getDefinition((string) $alias), array_merge($options, ['id' => (string) $alias])); } - /** - * {@inheritdoc} - */ protected function describeContainerParameter($parameter, array $options = []) { $options['output']->table( @@ -403,9 +380,6 @@ protected function describeContainerParameter($parameter, array $options = []) ]); } - /** - * {@inheritdoc} - */ protected function describeContainerEnvVars(array $envs, array $options = []) { $dump = new Dumper($this->output); @@ -468,12 +442,9 @@ protected function describeContainerEnvVars(array $envs, array $options = []) } } - /** - * {@inheritdoc} - */ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { - $event = \array_key_exists('event', $options) ? $options['event'] : null; + $event = $options['event'] ?? null; if (null !== $event) { $title = sprintf('Registered Listeners for "%s" Event', $event); @@ -495,20 +466,16 @@ protected function describeEventDispatcherListeners(EventDispatcherInterface $ev } } - /** - * {@inheritdoc} - */ protected function describeCallable($callable, array $options = []) { $this->writeText($this->formatCallable($callable), $options); } - private function renderEventListenerTable(EventDispatcherInterface $eventDispatcher, $event, array $eventListeners, SymfonyStyle $io) + private function renderEventListenerTable(EventDispatcherInterface $eventDispatcher, string $event, array $eventListeners, SymfonyStyle $io) { $tableHeaders = ['Order', 'Callable', 'Priority']; $tableRows = []; - $order = 1; foreach ($eventListeners as $order => $listener) { $tableRows[] = [sprintf('#%d', $order + 1), $this->formatCallable($listener), $eventDispatcher->getListenerPriority($event, $listener)]; } @@ -539,7 +506,9 @@ private function formatControllerLink($controller, string $anchorText): string } try { - if (\is_array($controller)) { + if (null === $controller) { + return $anchorText; + } elseif (\is_array($controller)) { $r = new \ReflectionMethod($controller[0], $controller[1]); } elseif ($controller instanceof \Closure) { $r = new \ReflectionFunction($controller); @@ -547,7 +516,7 @@ private function formatControllerLink($controller, string $anchorText): string $r = new \ReflectionMethod($controller, '__invoke'); } elseif (!\is_string($controller)) { return $anchorText; - } elseif (false !== strpos($controller, '::')) { + } elseif (str_contains($controller, '::')) { $r = new \ReflectionMethod($controller); } else { $r = new \ReflectionFunction($controller); @@ -580,7 +549,7 @@ private function formatCallable($callable): string if ($callable instanceof \Closure) { $r = new \ReflectionFunction($callable); - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return 'Closure()'; } if ($class = $r->getClosureScopeClass()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php index e7e52f0b9d123..28044126f9a7f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php +++ b/src/Symfony/Bundle/FrameworkBundle/Console/Descriptor/XmlDescriptor.php @@ -31,41 +31,26 @@ */ class XmlDescriptor extends Descriptor { - /** - * {@inheritdoc} - */ protected function describeRouteCollection(RouteCollection $routes, array $options = []) { $this->writeDocument($this->getRouteCollectionDocument($routes)); } - /** - * {@inheritdoc} - */ protected function describeRoute(Route $route, array $options = []) { - $this->writeDocument($this->getRouteDocument($route, isset($options['name']) ? $options['name'] : null)); + $this->writeDocument($this->getRouteDocument($route, $options['name'] ?? null)); } - /** - * {@inheritdoc} - */ protected function describeContainerParameters(ParameterBag $parameters, array $options = []) { $this->writeDocument($this->getContainerParametersDocument($parameters)); } - /** - * {@inheritdoc} - */ protected function describeContainerTags(ContainerBuilder $builder, array $options = []) { $this->writeDocument($this->getContainerTagsDocument($builder, isset($options['show_hidden']) && $options['show_hidden'])); } - /** - * {@inheritdoc} - */ protected function describeContainerService($service, array $options = [], ContainerBuilder $builder = null) { if (!isset($options['id'])) { @@ -75,32 +60,25 @@ protected function describeContainerService($service, array $options = [], Conta $this->writeDocument($this->getContainerServiceDocument($service, $options['id'], $builder, isset($options['show_arguments']) && $options['show_arguments'])); } - /** - * {@inheritdoc} - */ protected function describeContainerServices(ContainerBuilder $builder, array $options = []) { - $this->writeDocument($this->getContainerServicesDocument($builder, isset($options['tag']) ? $options['tag'] : null, isset($options['show_hidden']) && $options['show_hidden'], isset($options['show_arguments']) && $options['show_arguments'], isset($options['filter']) ? $options['filter'] : null)); + $this->writeDocument($this->getContainerServicesDocument($builder, $options['tag'] ?? null, isset($options['show_hidden']) && $options['show_hidden'], isset($options['show_arguments']) && $options['show_arguments'], $options['filter'] ?? null)); } - /** - * {@inheritdoc} - */ protected function describeContainerDefinition(Definition $definition, array $options = []) { - $this->writeDocument($this->getContainerDefinitionDocument($definition, isset($options['id']) ? $options['id'] : null, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'])); + $this->writeDocument($this->getContainerDefinitionDocument($definition, $options['id'] ?? null, isset($options['omit_tags']) && $options['omit_tags'], isset($options['show_arguments']) && $options['show_arguments'])); } - /** - * {@inheritdoc} - */ protected function describeContainerAlias(Alias $alias, array $options = [], ContainerBuilder $builder = null) { $dom = new \DOMDocument('1.0', 'UTF-8'); - $dom->appendChild($dom->importNode($this->getContainerAliasDocument($alias, isset($options['id']) ? $options['id'] : null)->childNodes->item(0), true)); + $dom->appendChild($dom->importNode($this->getContainerAliasDocument($alias, $options['id'] ?? null)->childNodes->item(0), true)); if (!$builder) { - return $this->writeDocument($dom); + $this->writeDocument($dom); + + return; } $dom->appendChild($dom->importNode($this->getContainerDefinitionDocument($builder->getDefinition((string) $alias), (string) $alias)->childNodes->item(0), true)); @@ -108,43 +86,26 @@ protected function describeContainerAlias(Alias $alias, array $options = [], Con $this->writeDocument($dom); } - /** - * {@inheritdoc} - */ protected function describeEventDispatcherListeners(EventDispatcherInterface $eventDispatcher, array $options = []) { - $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, \array_key_exists('event', $options) ? $options['event'] : null)); + $this->writeDocument($this->getEventDispatcherListenersDocument($eventDispatcher, $options['event'] ?? null)); } - /** - * {@inheritdoc} - */ protected function describeCallable($callable, array $options = []) { $this->writeDocument($this->getCallableDocument($callable)); } - /** - * {@inheritdoc} - */ protected function describeContainerParameter($parameter, array $options = []) { $this->writeDocument($this->getContainerParameterDocument($parameter, $options)); } - /** - * {@inheritdoc} - */ protected function describeContainerEnvVars(array $envs, array $options = []) { throw new LogicException('Using the XML format to debug environment variables is not supported.'); } - /** - * Writes DOM document. - * - * @return \DOMDocument|string - */ private function writeDocument(\DOMDocument $dom) { $dom->formatOutput = true; @@ -289,13 +250,14 @@ private function getContainerServicesDocument(ContainerBuilder $builder, string $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($containerXML = $dom->createElement('container')); - $serviceIds = $tag ? array_keys($builder->findTaggedServiceIds($tag)) : $builder->getServiceIds(); - + $serviceIds = $tag + ? $this->sortTaggedServicesByPriority($builder->findTaggedServiceIds($tag)) + : $this->sortServiceIds($builder->getServiceIds()); if ($filter) { $serviceIds = array_filter($serviceIds, $filter); } - foreach ($this->sortServiceIds($serviceIds) as $serviceId) { + foreach ($serviceIds as $serviceId) { $service = $this->resolveServiceDefinition($builder, $serviceId); if ($showHidden xor '.' === ($serviceId[0] ?? null)) { @@ -323,7 +285,7 @@ private function getContainerDefinitionDocument(Definition $definition, string $ $descriptionXML->appendChild($dom->createCDATASection($classDescription)); } - $serviceXML->setAttribute('class', $definition->getClass()); + $serviceXML->setAttribute('class', $definition->getClass() ?? ''); if ($factory = $definition->getFactory()) { $serviceXML->appendChild($factoryXML = $dom->createElement('factory')); @@ -332,7 +294,7 @@ private function getContainerDefinitionDocument(Definition $definition, string $ if ($factory[0] instanceof Reference) { $factoryXML->setAttribute('service', (string) $factory[0]); } elseif ($factory[0] instanceof Definition) { - throw new \InvalidArgumentException('Factory is not describable.'); + $factoryXML->setAttribute('service', sprintf('inline factory service (%s)', $factory[0]->getClass() ?? 'not configured')); } else { $factoryXML->setAttribute('class', $factory[0]); } @@ -349,7 +311,7 @@ private function getContainerDefinitionDocument(Definition $definition, string $ $serviceXML->setAttribute('abstract', $definition->isAbstract() ? 'true' : 'false'); $serviceXML->setAttribute('autowired', $definition->isAutowired() ? 'true' : 'false'); $serviceXML->setAttribute('autoconfigured', $definition->isAutoconfigured() ? 'true' : 'false'); - $serviceXML->setAttribute('file', $definition->getFile()); + $serviceXML->setAttribute('file', $definition->getFile() ?? ''); $calls = $definition->getMethodCalls(); if (\count($calls) > 0) { @@ -370,7 +332,7 @@ private function getContainerDefinitionDocument(Definition $definition, string $ } if (!$omitTags) { - if ($tags = $definition->getTags()) { + if ($tags = $this->sortTagsByPriority($definition->getTags())) { $serviceXML->appendChild($tagsXML = $dom->createElement('tags')); foreach ($tags as $tagName => $tagData) { foreach ($tagData as $parameters) { @@ -392,7 +354,7 @@ private function getContainerDefinitionDocument(Definition $definition, string $ /** * @return \DOMNode[] */ - private function getArgumentNodes(array $arguments, \DOMDocument $dom) + private function getArgumentNodes(array $arguments, \DOMDocument $dom): array { $nodes = []; @@ -421,9 +383,12 @@ private function getArgumentNodes(array $arguments, \DOMDocument $dom) } elseif (\is_array($argument)) { $argumentXML->setAttribute('type', 'collection'); - foreach ($this->getArgumentNodes($argument, $dom) as $childArgumenXML) { - $argumentXML->appendChild($childArgumenXML); + foreach ($this->getArgumentNodes($argument, $dom) as $childArgumentXML) { + $argumentXML->appendChild($childArgumentXML); } + } elseif ($argument instanceof \UnitEnum) { + $argumentXML->setAttribute('type', 'constant'); + $argumentXML->appendChild(new \DOMText(ltrim(var_export($argument, true), '\\'))); } else { $argumentXML->appendChild(new \DOMText($argument)); } @@ -449,7 +414,7 @@ private function getContainerAliasDocument(Alias $alias, string $id = null): \DO return $dom; } - private function getContainerParameterDocument($parameter, $options = []): \DOMDocument + private function getContainerParameterDocument($parameter, array $options = []): \DOMDocument { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->appendChild($parameterXML = $dom->createElement('parameter')); @@ -485,7 +450,7 @@ private function getEventDispatcherListenersDocument(EventDispatcherInterface $e return $dom; } - private function appendEventListenerDocument(EventDispatcherInterface $eventDispatcher, $event, \DOMElement $element, array $eventListeners) + private function appendEventListenerDocument(EventDispatcherInterface $eventDispatcher, string $event, \DOMElement $element, array $eventListeners) { foreach ($eventListeners as $listener) { $callableXML = $this->getCallableDocument($listener); @@ -507,7 +472,7 @@ private function getCallableDocument($callable): \DOMDocument $callableXML->setAttribute('name', $callable[1]); $callableXML->setAttribute('class', \get_class($callable[0])); } else { - if (0 !== strpos($callable[1], 'parent::')) { + if (!str_starts_with($callable[1], 'parent::')) { $callableXML->setAttribute('name', $callable[1]); $callableXML->setAttribute('class', $callable[0]); $callableXML->setAttribute('static', 'true'); @@ -525,7 +490,7 @@ private function getCallableDocument($callable): \DOMDocument if (\is_string($callable)) { $callableXML->setAttribute('type', 'function'); - if (false === strpos($callable, '::')) { + if (!str_contains($callable, '::')) { $callableXML->setAttribute('name', $callable); } else { $callableParts = explode('::', $callable); @@ -542,7 +507,7 @@ private function getCallableDocument($callable): \DOMDocument $callableXML->setAttribute('type', 'closure'); $r = new \ReflectionFunction($callable); - if (false !== strpos($r->name, '{closure}')) { + if (str_contains($r->name, '{closure}')) { return $dom; } $callableXML->setAttribute('name', $r->name); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php index ac7ab231e01a9..5a2112daf9ed2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/AbstractController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; -use Doctrine\Common\Persistence\ManagerRegistry; +use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBagInterface; @@ -47,7 +47,7 @@ abstract class AbstractController implements ServiceSubscriberInterface * @internal * @required */ - public function setContainer(ContainerInterface $container) + public function setContainer(ContainerInterface $container): ?ContainerInterface { $previous = $this->container; $this->container = $container; @@ -58,14 +58,14 @@ public function setContainer(ContainerInterface $container) /** * Gets a container parameter by its name. * - * @return mixed + * @return array|bool|float|int|string|\UnitEnum|null * * @final */ protected function getParameter(string $name) { if (!$this->container->has('parameter_bag')) { - throw new ServiceNotFoundException('parameter_bag', null, null, [], sprintf('The "%s::getParameter()" method is missing a parameter bag to work properly. Did you forget to register your controller as a service subscriber? This can be fixed either by using autoconfiguration or by manually wiring a "parameter_bag" in the service locator passed to the controller.', \get_class($this))); + throw new ServiceNotFoundException('parameter_bag.', null, null, [], sprintf('The "%s::getParameter()" method is missing a parameter bag to work properly. Did you forget to register your controller as a service subscriber? This can be fixed either by using autoconfiguration or by manually wiring a "parameter_bag" in the service locator passed to the controller.', static::class)); } return $this->container->get('parameter_bag')->get($name); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php index b6708db544506..a7d8f9425c0d0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/Controller.php @@ -31,7 +31,7 @@ abstract class Controller implements ContainerAwareInterface /** * Gets a container configuration parameter by its name. * - * @return mixed + * @return array|bool|float|int|string|\UnitEnum|null * * @final */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php index 1a1112dbaeb23..34e4382ad8f96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerNameParser.php @@ -32,7 +32,7 @@ public function __construct(KernelInterface $kernel, bool $triggerDeprecation = $this->kernel = $kernel; if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); } } @@ -49,7 +49,7 @@ public function __construct(KernelInterface $kernel, bool $triggerDeprecation = public function parse($controller) { if (2 > \func_num_args() || func_get_arg(1)) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); } $parts = explode(':', $controller); @@ -58,7 +58,7 @@ public function parse($controller) } $originalController = $controller; - list($bundleName, $controller, $action) = $parts; + [$bundleName, $controller, $action] = $parts; $controller = str_replace('/', '\\', $controller); try { @@ -97,7 +97,7 @@ public function parse($controller) */ public function build($controller) { - @trigger_error(sprintf('The %s class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The %s class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); if (0 === preg_match('#^(.*?\\\\Controller\\\\(.+)Controller)::(.+)Action$#', $controller, $match)) { throw new \InvalidArgumentException(sprintf('The "%s" controller is not a valid "class::method" string.', $controller)); @@ -107,7 +107,7 @@ public function build($controller) $controllerName = $match[2]; $actionName = $match[3]; foreach ($this->kernel->getBundles() as $name => $bundle) { - if (0 !== strpos($className, $bundle->getNamespace())) { + if (!str_starts_with($className, $bundle->getNamespace())) { continue; } @@ -130,7 +130,7 @@ private function findAlternative(string $nonExistentBundleName): ?string $shortest = null; foreach ($bundleNames as $bundleName) { // if there's a partial match, return it immediately - if (false !== strpos($bundleName, $nonExistentBundleName)) { + if (str_contains($bundleName, $nonExistentBundleName)) { return $bundleName; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php index e4f5e5dfa54a3..2883c80b1b0ba 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerResolver.php @@ -34,13 +34,13 @@ class ControllerResolver extends ContainerControllerResolver public function __construct(ContainerInterface $container, $logger = null) { if ($logger instanceof ControllerNameParser) { - @trigger_error(sprintf('Passing a "%s" instance as 2nd argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance or null instead.', ControllerNameParser::class, __METHOD__, LoggerInterface::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing a "%s" instance as 2nd argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance or null instead.', ControllerNameParser::class, __METHOD__, LoggerInterface::class), \E_USER_DEPRECATED); $this->parser = $logger; $logger = 2 < \func_num_args() ? func_get_arg(2) : null; } elseif (2 < \func_num_args() && func_get_arg(2) instanceof ControllerNameParser) { $this->parser = func_get_arg(2); } elseif ($logger && !$logger instanceof LoggerInterface) { - throw new \TypeError(sprintf('Argument 2 of "%s()" must be an instance of "%s" or null, "%s" given.', __METHOD__, LoggerInterface::class, \is_object($logger) ? \get_class($logger) : \gettype($logger)), E_USER_DEPRECATED); + throw new \TypeError(sprintf('Argument 2 of "%s()" must be an instance of "%s" or null, "%s" given.', __METHOD__, LoggerInterface::class, \is_object($logger) ? \get_class($logger) : \gettype($logger)), \E_USER_DEPRECATED); } parent::__construct($container, $logger); @@ -51,12 +51,12 @@ public function __construct(ContainerInterface $container, $logger = null) */ protected function createController($controller) { - if ($this->parser && false === strpos($controller, '::') && 2 === substr_count($controller, ':')) { + if ($this->parser && !str_contains($controller, '::') && 2 === substr_count($controller, ':')) { // controller in the a:b:c notation then $deprecatedNotation = $controller; $controller = $this->parser->parse($deprecatedNotation, false); - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1. Use %s instead.', $deprecatedNotation, $controller), E_USER_DEPRECATED); + @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1. Use %s instead.', $deprecatedNotation, $controller), \E_USER_DEPRECATED); } return parent::createController($controller); @@ -67,17 +67,14 @@ protected function createController($controller) */ protected function instantiateController($class) { - return $this->configureController(parent::instantiateController($class), $class); - } + $controller = parent::instantiateController($class); - private function configureController($controller, string $class) - { if ($controller instanceof ContainerAwareInterface) { $controller->setContainer($this->container); } if ($controller instanceof AbstractController) { if (null === $previousContainer = $controller->setContainer($this->container)) { - @trigger_error(sprintf('Auto-injection of the container for "%s" is deprecated since Symfony 4.2. Configure it as a service instead.', $class), E_USER_DEPRECATED); + @trigger_error(sprintf('Auto-injection of the container for "%s" is deprecated since Symfony 4.2. Configure it as a service instead.', $class), \E_USER_DEPRECATED); // To be uncommented on Symfony 5: //throw new \LogicException(sprintf('"%s" has no container set, did you forget to define it as a service subscriber?', $class)); } else { diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php index 9cb7a58f6e856..8845e03d9a236 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/ControllerTrait.php @@ -11,10 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Controller; -use Doctrine\Common\Persistence\ManagerRegistry; -use Fig\Link\GenericLinkProvider; -use Fig\Link\Link; +use Doctrine\Persistence\ManagerRegistry; use Psr\Container\ContainerInterface; +use Psr\Link\LinkInterface; use Symfony\Component\Form\Extension\Core\Type\FormType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormInterface; @@ -31,8 +30,10 @@ use Symfony\Component\Messenger\Stamp\StampInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Core\Exception\AccessDeniedException; +use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Csrf\CsrfToken; use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener; +use Symfony\Component\WebLink\GenericLinkProvider; /** * Common features needed in controllers. @@ -143,7 +144,7 @@ protected function json($data, int $status = 200, array $headers = [], array $co protected function file($file, string $fileName = null, string $disposition = ResponseHeaderBag::DISPOSITION_ATTACHMENT): BinaryFileResponse { $response = new BinaryFileResponse($file); - $response->setContentDisposition($disposition, null === $fileName ? $response->getFile()->getFilename() : $fileName); + $response->setContentDisposition($disposition, $fileName ?? $response->getFile()->getFilename()); return $response; } @@ -155,7 +156,7 @@ protected function file($file, string $fileName = null, string $disposition = Re * * @final */ - protected function addFlash(string $type, string $message) + protected function addFlash(string $type, $message) { if (!$this->container->has('session')) { throw new \LogicException('You can not use the addFlash method if sessions are disabled. Enable them in "config/packages/framework.yaml".'); @@ -206,8 +207,8 @@ protected function denyAccessUnlessGranted($attributes, $subject = null, string */ protected function renderView(string $view, array $parameters = []): string { - if ($this->container->has('templating')) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + if ($this->container->has('templating') && $this->container->get('templating')->supports($view)) { + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); return $this->container->get('templating')->render($view, $parameters); } @@ -226,8 +227,8 @@ protected function renderView(string $view, array $parameters = []): string */ protected function render(string $view, array $parameters = [], Response $response = null): Response { - if ($this->container->has('templating')) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + if ($this->container->has('templating') && $this->container->get('templating')->supports($view)) { + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); $content = $this->container->get('templating')->render($view, $parameters); } elseif ($this->container->has('twig')) { @@ -253,7 +254,7 @@ protected function render(string $view, array $parameters = [], Response $respon protected function stream(string $view, array $parameters = [], StreamedResponse $response = null): StreamedResponse { if ($this->container->has('templating')) { - @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + @trigger_error('Using the "templating" service is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); $templating = $this->container->get('templating'); @@ -288,7 +289,7 @@ protected function stream(string $view, array $parameters = [], StreamedResponse * * @final */ - protected function createNotFoundException(string $message = 'Not Found', \Exception $previous = null): NotFoundHttpException + protected function createNotFoundException(string $message = 'Not Found', \Throwable $previous = null): NotFoundHttpException { return new NotFoundHttpException($message, $previous); } @@ -304,7 +305,7 @@ protected function createNotFoundException(string $message = 'Not Found', \Excep * * @final */ - protected function createAccessDeniedException(string $message = 'Access Denied.', \Exception $previous = null): AccessDeniedException + protected function createAccessDeniedException(string $message = 'Access Denied.', \Throwable $previous = null): AccessDeniedException { if (!class_exists(AccessDeniedException::class)) { throw new \LogicException('You can not use the "createAccessDeniedException" method if the Security component is not available. Try running "composer require symfony/security-bundle".'); @@ -336,11 +337,13 @@ protected function createFormBuilder($data = null, array $options = []): FormBui /** * Shortcut to return the Doctrine Registry service. * + * @return ManagerRegistry + * * @throws \LogicException If DoctrineBundle is not available * * @final */ - protected function getDoctrine(): ManagerRegistry + protected function getDoctrine() { if (!$this->container->has('doctrine')) { throw new \LogicException('The DoctrineBundle is not registered in your application. Try running "composer require symfony/orm-pack".'); @@ -352,7 +355,7 @@ protected function getDoctrine(): ManagerRegistry /** * Get a user from the Security Token Storage. * - * @return mixed + * @return UserInterface|object|null * * @throws \LogicException If SecurityBundle is not available * @@ -367,12 +370,12 @@ protected function getUser() } if (null === $token = $this->container->get('security.token_storage')->getToken()) { - return; + return null; } if (!\is_object($user = $token->getUser())) { // e.g. anonymous authentication - return; + return null; } return $user; @@ -420,7 +423,7 @@ protected function dispatchMessage($message, array $stamps = []): Envelope * * @final */ - protected function addLink(Request $request, Link $link) + protected function addLink(Request $request, LinkInterface $link) { if (!class_exists(AddLinkHeaderListener::class)) { throw new \LogicException('You can not use the "addLink" method if the WebLink component is not available. Try running "composer require symfony/web-link".'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php index d669771a59edb..109e83b6967ba 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/RedirectController.php @@ -46,7 +46,6 @@ public function __construct(UrlGeneratorInterface $router = null, int $httpPort * In case the route name is empty, the status code will be 404 when permanent is false * and 410 otherwise. * - * @param Request $request The request instance * @param string $route The route name to redirect to * @param bool $permanent Whether the redirection is permanent * @param bool|array $ignoreAttributes Whether to ignore attributes or an array of attributes to ignore @@ -63,7 +62,17 @@ public function redirectAction(Request $request, string $route, bool $permanent $attributes = []; if (false === $ignoreAttributes || \is_array($ignoreAttributes)) { $attributes = $request->attributes->get('_route_params'); - $attributes = $keepQueryParams ? array_merge($request->query->all(), $attributes) : $attributes; + + if ($keepQueryParams) { + if ($query = $request->server->get('QUERY_STRING')) { + $query = self::parseQuery($query); + } else { + $query = $request->query->all(); + } + + $attributes = array_merge($query, $attributes); + } + unset($attributes['route'], $attributes['permanent'], $attributes['ignoreAttributes'], $attributes['keepRequestMethod'], $attributes['keepQueryParams']); if ($ignoreAttributes) { $attributes = array_diff_key($attributes, array_flip($ignoreAttributes)); @@ -88,7 +97,6 @@ public function redirectAction(Request $request, string $route, bool $permanent * In case the path is empty, the status code will be 404 when permanent is false * and 410 otherwise. * - * @param Request $request The request instance * @param string $path The absolute path or URL to redirect to * @param bool $permanent Whether the redirect is permanent or not * @param string|null $scheme The URL scheme (null to keep the current one) @@ -111,7 +119,7 @@ public function urlRedirectAction(Request $request, string $path, bool $permanen } // redirect if the path is a full URL - if (parse_url($path, PHP_URL_SCHEME)) { + if (parse_url($path, \PHP_URL_SCHEME)) { return new RedirectResponse($path, $statusCode); } @@ -119,9 +127,8 @@ public function urlRedirectAction(Request $request, string $path, bool $permanen $scheme = $request->getScheme(); } - $qs = $request->getQueryString(); - if ($qs) { - if (false === strpos($path, '?')) { + if ($qs = $request->server->get('QUERY_STRING') ?: $request->getQueryString()) { + if (!str_contains($path, '?')) { $qs = '?'.$qs; } else { $qs = '&'.$qs; @@ -159,4 +166,68 @@ public function urlRedirectAction(Request $request, string $path, bool $permanen return new RedirectResponse($url, $statusCode); } + + public function __invoke(Request $request): Response + { + $p = $request->attributes->get('_route_params', []); + + if (\array_key_exists('route', $p)) { + if (\array_key_exists('path', $p)) { + throw new \RuntimeException(sprintf('Ambiguous redirection settings, use the "path" or "route" parameter, not both: "%s" and "%s" found respectively in "%s" routing configuration.', $p['path'], $p['route'], $request->attributes->get('_route'))); + } + + return $this->redirectAction($request, $p['route'], $p['permanent'] ?? false, $p['ignoreAttributes'] ?? false, $p['keepRequestMethod'] ?? false, $p['keepQueryParams'] ?? false); + } + + if (\array_key_exists('path', $p)) { + return $this->urlRedirectAction($request, $p['path'], $p['permanent'] ?? false, $p['scheme'] ?? null, $p['httpPort'] ?? null, $p['httpsPort'] ?? null, $p['keepRequestMethod'] ?? false); + } + + throw new \RuntimeException(sprintf('The parameter "path" or "route" is required to configure the redirect action in "%s" routing configuration.', $request->attributes->get('_route'))); + } + + private static function parseQuery(string $query) + { + $q = []; + + foreach (explode('&', $query) as $v) { + if (false !== $i = strpos($v, "\0")) { + $v = substr($v, 0, $i); + } + + if (false === $i = strpos($v, '=')) { + $k = urldecode($v); + $v = ''; + } else { + $k = urldecode(substr($v, 0, $i)); + $v = substr($v, $i); + } + + if (false !== $i = strpos($k, "\0")) { + $k = substr($k, 0, $i); + } + + $k = ltrim($k, ' '); + + if (false === $i = strpos($k, '[')) { + $q[] = bin2hex($k).$v; + } else { + $q[] = bin2hex(substr($k, 0, $i)).rawurlencode(substr($k, $i)).$v; + } + } + + parse_str(implode('&', $q), $q); + + $query = []; + + foreach ($q as $k => $v) { + if (false !== $i = strpos($k, '_')) { + $query[substr_replace($k, hex2bin(substr($k, 0, $i)).'[', 0, 1 + $i)] = $v; + } else { + $query[hex2bin($k)] = $v; + } + } + + return $query; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php index 8e359569f8ced..705adc8765a26 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Controller/TemplateController.php @@ -30,7 +30,7 @@ class TemplateController public function __construct(Environment $twig = null, EngineInterface $templating = null) { if (null !== $templating) { - @trigger_error(sprintf('Using a "%s" instance for "%s" is deprecated since version 4.4; use a \Twig\Environment instance instead.', EngineInterface::class, __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('Using a "%s" instance for "%s" is deprecated since version 4.4; use a \Twig\Environment instance instead.', EngineInterface::class, __CLASS__), \E_USER_DEPRECATED); } $this->twig = $twig; @@ -55,17 +55,17 @@ public function templateAction(string $template, int $maxAge = null, int $shared throw new \LogicException('You can not use the TemplateController if the Templating Component or the Twig Bundle are not available.'); } - if ($maxAge) { + if (null !== $maxAge) { $response->setMaxAge($maxAge); } - if ($sharedAge) { + if (null !== $sharedAge) { $response->setSharedMaxAge($sharedAge); } if ($private) { $response->setPrivate(); - } elseif (false === $private || (null === $private && ($maxAge || $sharedAge))) { + } elseif (false === $private || (null === $private && (null !== $maxAge || null !== $sharedAge))) { $response->setPublic(); } diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php index 3e82f27e2c585..a112f890d7a7a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RequestDataCollector.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector as BaseRequestDataCollector; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1. Use %s instead.', RequestDataCollector::class, BaseRequestDataCollector::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1. Use %s instead.', RequestDataCollector::class, BaseRequestDataCollector::class), \E_USER_DEPRECATED); /** * RequestDataCollector. diff --git a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php index 90a88ca10e313..60681f7291f55 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php +++ b/src/Symfony/Bundle/FrameworkBundle/DataCollector/RouterDataCollector.php @@ -16,9 +16,9 @@ use Symfony\Component\HttpKernel\DataCollector\RouterDataCollector as BaseRouterDataCollector; /** - * RouterDataCollector. - * * @author Fabien Potencier + * + * @final since Symfony 4.4 */ class RouterDataCollector extends BaseRouterDataCollector { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php index 4f09e52bdcbd1..d7db6ebf050a4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddAnnotationsCachedReaderPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler; +use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -34,8 +35,11 @@ public function process(ContainerBuilder $container) $provider = $properties['cacheProviderBackup']->getValues()[0]; unset($properties['cacheProviderBackup']); $reader->setProperties($properties); - $container->set($id, null); - $container->setDefinition($id, $reader->replaceArgument(1, $provider)); + $reader->replaceArgument(1, $provider); + } elseif (4 <= \count($arguments = $reader->getArguments()) && $arguments[3] instanceof ServiceClosureArgument) { + $arguments[1] = $arguments[3]->getValues()[0]; + unset($arguments[3]); + $reader->setArguments($arguments); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php index 7c6c85f64b2b9..3c44205e3e55b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php @@ -28,7 +28,7 @@ class AddExpressionLanguageProvidersPass implements CompilerPassInterface public function __construct(bool $handleSecurityLanguageProviders = true) { if ($handleSecurityLanguageProviders) { - @trigger_error(sprintf('Registering services tagged "security.expression_language_provider" with "%s" is deprecated since Symfony 4.2, use the "%s" instead.', __CLASS__, SecurityExpressionLanguageProvidersPass::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Registering services tagged "security.expression_language_provider" with "%s" is deprecated since Symfony 4.2, use the "%s" instead.', __CLASS__, SecurityExpressionLanguageProvidersPass::class), \E_USER_DEPRECATED); } $this->handleSecurityLanguageProviders = $handleSecurityLanguageProviders; diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php index 7be7464f710de..93d13834bd3f6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CacheCollectorPass.php @@ -13,7 +13,7 @@ use Symfony\Component\Cache\DependencyInjection\CacheCollectorPass as BaseCacheCollectorPass; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CacheCollectorPass::class, BaseCacheCollectorPass::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CacheCollectorPass::class, BaseCacheCollectorPass::class), \E_USER_DEPRECATED); /** * Inject a data collector to all the cache services to be able to get detailed statistics. diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php index c43cb769dbf9c..58e23ace112cb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolClearerPass.php @@ -13,7 +13,7 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolClearerPass as BaseCachePoolClearerPass; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolClearerPass::class, BaseCachePoolClearerPass::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolClearerPass::class, BaseCachePoolClearerPass::class), \E_USER_DEPRECATED); /** * @author Nicolas Grekas diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php index bc7ab8bcc140a..30be3be53bd2c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPass.php @@ -13,7 +13,7 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPass as BaseCachePoolPass; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPass::class, BaseCachePoolPass::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPass::class, BaseCachePoolPass::class), \E_USER_DEPRECATED); /** * @author Nicolas Grekas diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php index d20c93a024a7c..22237ec01cc6b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/CachePoolPrunerPass.php @@ -13,7 +13,7 @@ use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass as BaseCachePoolPrunerPass; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPrunerPass::class, BaseCachePoolPrunerPass::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use "%s" instead.', CachePoolPrunerPass::class, BaseCachePoolPrunerPass::class), \E_USER_DEPRECATED); /** * @author Rob Frawley 2nd diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php index 357c079c4aa0b..78140985825cc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/ProfilerPass.php @@ -32,14 +32,14 @@ public function process(ContainerBuilder $container) $definition = $container->getDefinition('profiler'); $collectors = new \SplPriorityQueue(); - $order = PHP_INT_MAX; + $order = \PHP_INT_MAX; foreach ($container->findTaggedServiceIds('data_collector', true) as $id => $attributes) { - $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $priority = $attributes[0]['priority'] ?? 0; $template = null; if (isset($attributes[0]['template'])) { if (!isset($attributes[0]['id'])) { - throw new InvalidArgumentException(sprintf('Data collector service "%s" must have an id attribute in order to specify a template', $id)); + throw new InvalidArgumentException(sprintf('Data collector service "%s" must have an id attribute in order to specify a template.', $id)); } $template = [$attributes[0]['id'], $attributes[0]['template']]; } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.php new file mode 100644 index 0000000000000..0f4950615fbce --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/SessionPass.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\Bundle\FrameworkBundle\DependencyInjection\Compiler; + +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * @internal to be removed in 6.0 + */ +class SessionPass implements CompilerPassInterface +{ + public function process(ContainerBuilder $container) + { + if (!$container->hasDefinition('session')) { + return; + } + + $bags = [ + 'session.flash_bag' => $container->hasDefinition('session.flash_bag') ? $container->getDefinition('session.flash_bag') : null, + 'session.attribute_bag' => $container->hasDefinition('session.attribute_bag') ? $container->getDefinition('session.attribute_bag') : null, + ]; + + foreach ($container->getDefinition('session')->getArguments() as $v) { + if (!$v instanceof Reference || !isset($bags[$bag = (string) $v]) || !\is_array($factory = $bags[$bag]->getFactory())) { + continue; + } + + if ([0, 1] !== array_keys($factory) || !$factory[0] instanceof Reference || 'session' !== (string) $factory[0]) { + continue; + } + + if ('get'.ucfirst(substr($bag, 8, -4)).'Bag' !== $factory[1]) { + continue; + } + + $bags[$bag]->setFactory(null); + } + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php index be7418d909726..cbbce7e31eccf 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TemplatingPass.php @@ -40,7 +40,7 @@ public function process(ContainerBuilder $container) foreach ($container->findTaggedServiceIds('templating.helper', true) as $id => $attributes) { if (!$container->getDefinition($id)->isDeprecated()) { - @trigger_error('The "templating.helper" tag is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + @trigger_error('The "templating.helper" tag is deprecated since version 4 10000 .3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); } if (isset($attributes[0]['alias'])) { diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php index efeafad5f06e0..669d331c062f3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php @@ -21,34 +21,50 @@ */ class UnusedTagsPass implements CompilerPassInterface { - private $whitelist = [ + private $knownTags = [ 'annotations.cached_reader', + 'auto_alias', + 'cache.pool', 'cache.pool.clearer', + 'config_cache.resource_checker', 'console.command', + 'container.do_not_inline', + 'container.env_var_loader', + 'container.env_var_processor', 'container.hot_path', 'container.reversible', 'container.service_locator', + 'container.service_locator_context', 'container.service_subscriber', + 'controller.argument_value_resolver', 'controller.service_arguments', - 'config_cache.resource_checker', 'data_collector', 'form.type', 'form.type_extension', 'form.type_guesser', + 'http_client.client', 'kernel.cache_clearer', 'kernel.cache_warmer', 'kernel.event_listener', 'kernel.event_subscriber', 'kernel.fragment_renderer', 'kernel.locale_aware', + 'kernel.reset', + 'mailer.transport_factory', 'messenger.bus', - 'messenger.receiver', 'messenger.message_handler', + 'messenger.receiver', + 'messenger.transport_factory', 'mime.mime_type_guesser', 'monolog.logger', + 'property_info.access_extractor', + 'property_info.initializable_extractor', + 'property_info.list_extractor', + 'property_info.type_extractor', 'proxy', 'routing.expression_language_provider', 'routing.loader', + 'routing.route_loader', 'security.expression_language_provider', 'security.remember_me_aware', 'security.voter', @@ -60,17 +76,20 @@ class UnusedTagsPass implements CompilerPassInterface 'translation.loader', 'twig.extension', 'twig.loader', + 'twig.runtime', + 'validator.auto_mapper', 'validator.constraint_validator', 'validator.initializer', + 'workflow.definition', ]; public function process(ContainerBuilder $container) { - $tags = array_unique(array_merge($container->findTags(), $this->whitelist)); + $tags = array_unique(array_merge($container->findTags(), $this->knownTags)); foreach ($container->findUnusedTags() as $tag) { - // skip whitelisted tags - if (\in_array($tag, $this->whitelist)) { + // skip known tags + if (\in_array($tag, $this->knownTags)) { continue; } @@ -81,7 +100,7 @@ public function process(ContainerBuilder $container) continue; } - if (false !== strpos($definedTag, $tag) || levenshtein($tag, $definedTag) <= \strlen($tag) / 3) { + if (str_contains($definedTag, $tag) || levenshtein($tag, $definedTag) <= \strlen($tag) / 3) { $candidates[] = $definedTag; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php index 8a149e25515e5..3cef369a44347 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Configuration.php @@ -19,6 +19,7 @@ use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Form\Form; use Symfony\Component\HttpClient\HttpClient; @@ -28,7 +29,6 @@ use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\PropertyInfo\PropertyInfoExtractorInterface; -use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\Translator; use Symfony\Component\Validator\Validation; @@ -36,9 +36,6 @@ /** * FrameworkExtension configuration structure. - * - * @author Jeremy Mikola - * @author Grégoire Pineau */ class Configuration implements ConfigurationInterface { @@ -84,6 +81,9 @@ public function getConfigTreeBuilder() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() + ->scalarNode('error_controller') + ->defaultValue('error_controller') + ->end() ->end() ; @@ -113,10 +113,27 @@ public function getConfigTreeBuilder() $this->addRobotsIndexSection($rootNode); $this->addHttpClientSection($rootNode); $this->addMailerSection($rootNode); + $this->addSecretsSection($rootNode); return $treeBuilder; } + private function addSecretsSection(ArrayNodeDefinition $rootNode) + { + $rootNode + ->children() + ->arrayNode('secrets') + ->canBeDisabled() + ->children() + ->scalarNode('vault_directory')->defaultValue('%kernel.project_dir%/config/secrets/%kernel.environment%')->cannotBeEmpty()->end() + ->scalarNode('local_dotenv_file')->defaultValue('%kernel.project_dir%/.env.%kernel.environment%.local')->end() + ->scalarNode('decryption_env_var')->defaultValue('base64:default::SYMFONY_DECRYPTION_SECRET')->end() + ->end() + ->end() + ->end() + ; + } + private function addCsrfSection(ArrayNodeDefinition $rootNode) { $rootNode @@ -225,7 +242,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->canBeEnabled() ->beforeNormalization() ->always(function ($v) { - if (true === $v['enabled']) { + if (\is_array($v) && true === $v['enabled']) { $workflows = $v; unset($workflows['enabled']); @@ -286,7 +303,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->validate() ->ifTrue(function ($v) { return 'method' !== $v; }) ->then(function ($v) { - @trigger_error('Passing something else than "method" has been deprecated in Symfony 4.3.', E_USER_DEPRECATED); + @trigger_error('Passing something else than "method" has been deprecated in Symfony 4.3.', \E_USER_DEPRECATED); return $v; }) @@ -303,7 +320,7 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->scalarNode('property') - ->defaultValue('marking') + ->defaultNull() // In Symfony 5.0, set "marking" as default property ->end() ->scalarNode('service') ->cannotBeEmpty() @@ -476,11 +493,19 @@ private function addWorkflowSection(ArrayNodeDefinition $rootNode) return 'workflow' === $v['type'] && 'single_state' === ($v['marking_store']['type'] ?? false); }) ->then(function ($v) { - @trigger_error('Using a workflow with type=workflow and a marking_store=single_state is deprecated since Symfony 4.3. Use type=state_machine instead.', E_USER_DEPRECATED); + @trigger_error('Using a workflow with type=workflow and a marking_store=single_state is deprecated since Symfony 4.3. Use type=state_machine instead.', \E_USER_DEPRECATED); return $v; }) ->end() + ->validate() + ->ifTrue(function ($v) { + return isset($v['marking_store']['property']) + && (!isset($v['marking_store']['type']) || 'method' !== $v['marking_store']['type']) + ; + }) + ->thenInvalid('"property" option is only supported by the "method" marking store.') + ->end() ->end() ->end() ->end() @@ -522,12 +547,6 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) $rootNode ->children() ->arrayNode('session') - ->validate() - ->ifTrue(function ($v) { - return empty($v['handler_id']) && !empty($v['save_path']); - }) - ->thenInvalid('Session save path is ignored without a handler service') - ->end() ->info('session configuration') ->canBeEnabled() ->children() @@ -548,12 +567,12 @@ private function addSessionSection(ArrayNodeDefinition $rootNode) ->scalarNode('cookie_domain')->end() ->enumNode('cookie_secure')->values([true, false, 'auto'])->end() ->booleanNode('cookie_httponly')->defaultTrue()->end() - ->enumNode('cookie_samesite')->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT])->defaultNull()->end() + ->enumNode('cookie_samesite')->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT, Cookie::SAMESITE_NONE])->defaultNull()->end() ->booleanNode('use_cookies')->end() ->scalarNode('gc_divisor')->end() ->scalarNode('gc_probability')->defaultValue(1)->end() ->scalarNode('gc_maxlifetime')->end() - ->scalarNode('save_path')->end() + ->scalarNode('save_path')->defaultValue('%kernel.cache_dir%/sessions')->end() ->integerNode('metadata_update_threshold') ->defaultValue(0) ->info('seconds to wait between 2 session metadata updates') @@ -605,7 +624,7 @@ private function addTemplatingSection(ArrayNodeDefinition $rootNode) ->arrayNode('templating') ->info('templating configuration') ->canBeEnabled() - ->setDeprecated('The "%path%.%node%" configuration is deprecated since Symfony 4.3. Use the "twig" service directly instead.') + ->setDeprecated('The "%path%.%node%" configuration is deprecated since Symfony 4.3. Configure the "twig" section provided by the Twig Bundle instead.') ->beforeNormalization() ->ifTrue(function ($v) { return false === $v || \is_array($v) && false === $v['enabled']; }) ->then(function () { return ['enabled' => false, 'engines' => false]; }) @@ -762,6 +781,7 @@ private function addTranslatorSection(ArrayNodeDefinition $rootNode) ->end() ->booleanNode('logging')->defaultValue(false)->end() ->scalarNode('formatter')->defaultValue('translator.formatter.default')->end() + ->scalarNode('cache_dir')->defaultValue('%kernel.cache_dir%/translations')->end() ->scalarNode('default_path') ->info('The default path used to load translations') ->defaultValue('%kernel.project_dir%/translations') @@ -789,7 +809,7 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->beforeNormalization() ->ifTrue(function ($v) { return isset($v['strict_email']); }) ->then(function ($v) { - @trigger_error('The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead.', E_USER_DEPRECATED); + @trigger_error('The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead.', \E_USER_DEPRECATED); return $v; }) @@ -838,6 +858,11 @@ private function addValidationSection(ArrayNodeDefinition $rootNode) ->end() ->end() ->arrayNode('auto_mapping') + ->info('A collection of namespaces for which auto-mapping will be enabled by default, or null to opt-in with the EnableAutoMapping constraint.') + ->example([ + 'App\\Entity\\' => [], + 'App\\WithSpecificLoaders\\' => ['validator.property_info_loader'], + ]) ->useAttributeAsKey('namespace') ->normalizeKeys(false) ->beforeNormalization() @@ -987,12 +1012,14 @@ private function addCacheSection(ArrayNodeDefinition $rootNode) ->prototype('array') ->fixXmlConfig('adapter') ->beforeNormalization() - ->ifTrue(function ($v) { return (isset($v['adapters']) || \is_array($v['adapter'] ?? null)) && isset($v['provider']); }) - ->thenInvalid('Pool cannot have a "provider" while "adapter" is set to a map') + ->ifTrue(function ($v) { return isset($v['provider']) && \is_array($v['adapters'] ?? $v['adapter'] ?? null) && 1 < \count($v['adapters'] ?? $v['adapter']); }) + ->thenInvalid('Pool cannot have a "provider" while more than one adapter is defined') ->end() ->children() ->arrayNode('adapters') + ->performNoDeepMerging() ->info('One or more adapters to chain for creating the pool, defaults to "cache.app".') + ->beforeNormalization()->castToArray()->end() ->beforeNormalization() ->always()->then(function ($values) { if ([0] === array_keys($values) && \is_array($values[0])) { @@ -1077,7 +1104,11 @@ private function addLockSection(ArrayNodeDefinition $rootNode) ->ifString()->then(function ($v) { return ['enabled' => true, 'resources' => $v]; }) ->end() ->beforeNormalization() - ->ifTrue(function ($v) { return \is_array($v) && !isset($v['resources']); }) + ->ifTrue(function ($v) { return \is_array($v) && !isset($v['enabled']); }) + ->then(function ($v) { return $v + ['enabled' => true]; }) + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return \is_array($v) && !isset($v['resources']) && !isset($v['resource']); }) ->then(function ($v) { $e = $v['enabled']; unset($v['enabled']); @@ -1086,19 +1117,37 @@ private function addLockSection(ArrayNodeDefinition $rootNode) }) ->end() ->addDefaultsIfNotSet() + ->validate() + ->ifTrue(static function (array $config) { return $config['enabled'] && !$config['resources']; }) + ->thenInvalid('At least one resource must be defined.') + ->end() ->fixXmlConfig('resource') ->children() ->arrayNode('resources') - ->requiresAtLeastOneElement() + ->normalizeKeys(false) + ->useAttributeAsKey('name') ->defaultValue(['default' => [class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock']]) ->beforeNormalization() ->ifString()->then(function ($v) { return ['default' => $v]; }) ->end() ->beforeNormalization() ->ifTrue(function ($v) { return \is_array($v) && array_keys($v) === range(0, \count($v) - 1); }) - ->then(function ($v) { return ['default' => $v]; }) + ->then(function ($v) { + $resources = []; + foreach ($v as $resource) { + $resources = array_merge_recursive( + $resources, + \is_array($resource) && isset($resource['name']) + ? [$resource['name'] => $resource['value']] + : ['default' => $resource] + ); + } + + return $resources; + }) ->end() ->prototype('array') + ->performNoDeepMerging() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->prototype('scalar')->end() ->end() @@ -1134,6 +1183,10 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) ->ifTrue(function ($v) { return isset($v['buses']) && \count($v['buses']) > 1 && null === $v['default_bus']; }) ->thenInvalid('You must specify the "default_bus" if you define more than one bus.') ->end() + ->validate() + ->ifTrue(static function ($v): bool { return isset($v['buses']) && null !== $v['default_bus'] && !isset($v['buses'][$v['default_bus']]); }) + ->then(static function (array $v): void { throw new InvalidConfigurationException(sprintf('The specified default bus "%s" is not configured. Available buses are "%s".', $v['default_bus'], implode('", "', array_keys($v['buses'])))); }) + ->end() ->children() ->arrayNode('routing') ->normalizeKeys(false) @@ -1144,6 +1197,10 @@ private function addMessengerSection(ArrayNodeDefinition $rootNode) if (!\is_array($config)) { return []; } + // If XML config with only one routing attribute + if (2 === \count($config) && isset($config['message-class']) && isset($config['sender'])) { + $config = [0 => $config]; + } $newConfig = []; foreach ($config as $k => $v) { @@ -1165,6 +1222,7 @@ function ($a) { }) ->end() ->prototype('array') + ->performNoDeepMerging() ->children() ->arrayNode('senders') ->requiresAtLeastOneElement() @@ -1254,6 +1312,7 @@ function ($a) { ->defaultTrue() ->end() ->arrayNode('middleware') + ->performNoDeepMerging() ->beforeNormalization() ->ifTrue(function ($v) { return \is_string($v) || (\is_array($v) && !\is_int(key($v))); }) ->then(function ($v) { return [$v]; }) @@ -1270,7 +1329,7 @@ function ($a) { return $middleware; } if (1 < \count($middleware)) { - throw new \InvalidArgumentException(sprintf('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, %s given.', json_encode($middleware))); + throw new \InvalidArgumentException('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, '.json_encode($middleware).' given.'); } return [ @@ -1347,7 +1406,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) if (!\is_array($config)) { return []; } - if (!isset($config['host'])) { + if (!isset($config['host'], $config['value']) || \count($config) > 2) { return $config; } @@ -1364,13 +1423,16 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->info('A comma separated list of hosts that do not require a proxy to be reached.') ->end() ->floatNode('timeout') - ->info('Defaults to "default_socket_timeout" ini parameter.') + ->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.') + ->end() + ->floatNode('max_duration') + ->info('The maximum execution time for the request+response as a whole.') ->end() ->scalarNode('bindto') ->info('A network interface name, IP address, a host name or a UNIX socket to bind to.') ->end() ->booleanNode('verify_peer') - ->info('Indicates if the peer should be verified in a SSL/TLS context.') + ->info('Indicates if the peer should be verified in an SSL/TLS context.') ->end() ->booleanNode('verify_host') ->info('Indicates if the host should exist as a certificate common name.') @@ -1424,7 +1486,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->thenInvalid('Either "scope" or "base_uri" should be defined.') ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['query']) && !isset($v['base_uri']); }) + ->ifTrue(function ($v) { return !empty($v['query']) && !isset($v['base_uri']); }) ->thenInvalid('"query" applies to "base_uri" but no base URI is defined.') ->end() ->children() @@ -1453,7 +1515,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) if (!\is_array($config)) { return []; } - if (!isset($config['key'])) { + if (!isset($config['key'], $config['value']) || \count($config) > 2) { return $config; } @@ -1483,7 +1545,7 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) if (!\is_array($config)) { return []; } - if (!isset($config['host'])) { + if (!isset($config['host'], $config['value']) || \count($config) > 2) { return $config; } @@ -1500,13 +1562,16 @@ private function addHttpClientSection(ArrayNodeDefinition $rootNode) ->info('A comma separated list of hosts that do not require a proxy to be reached.') ->end() ->floatNode('timeout') - ->info('Defaults to "default_socket_timeout" ini parameter.') + ->info('The idle timeout, defaults to the "default_socket_timeout" ini parameter.') + ->end() + ->floatNode('max_duration') + ->info('The maximum execution time for the request+response as a whole.') ->end() ->scalarNode('bindto') ->info('A network interface name, IP address, a host name or a UNIX socket to bind to.') ->end() ->booleanNode('verify_peer') - ->info('Indicates if the peer should be verified in a SSL/TLS context.') + ->info('Indicates if the peer should be verified in an SSL/TLS context.') ->end() ->booleanNode('verify_host') ->info('Indicates if the host should exist as a certificate common name.') @@ -1554,8 +1619,17 @@ private function addMailerSection(ArrayNodeDefinition $rootNode) ->arrayNode('mailer') ->info('Mailer configuration') ->{!class_exists(FullStack::class) && class_exists(Mailer::class) ? 'canBeDisabled' : 'canBeEnabled'}() + ->validate() + ->ifTrue(function ($v) { return isset($v['dsn']) && \count($v['transports']); }) + ->thenInvalid('"dsn" and "transports" cannot be used together.') + ->end() + ->fixXmlConfig('transport') ->children() - ->scalarNode('dsn')->defaultValue('smtp://null')->end() + ->scalarNode('dsn')->defaultNull()->end() + ->arrayNode('transports') + ->useAttributeAsKey('name') + ->prototype('scalar')->end() + ->end() ->arrayNode('envelope') ->info('Mailer Envelope configuration') ->children() diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php index 6a3f6aa086747..c19144ba5f204 100644 --- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\DependencyInjection; use Doctrine\Common\Annotations\AnnotationRegistry; +use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; use Http\Client\HttpClient; use Psr\Cache\CacheItemPoolInterface; @@ -24,10 +25,10 @@ use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Routing\AnnotatedRouteControllerLoader; use Symfony\Bundle\FrameworkBundle\Routing\RedirectableUrlMatcher; +use Symfony\Bundle\FrameworkBundle\Routing\RouteLoaderInterface; use Symfony\Bundle\FullStack; use Symfony\Component\Asset\PackageInterface; use Symfony\Component\BrowserKit\AbstractBrowser; -use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ChainAdapter; @@ -49,6 +50,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; use Symfony\Component\DependencyInjection\EnvVarProcessorInterface; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\LogicException; @@ -73,20 +75,21 @@ use Symfony\Component\Lock\Lock; use Symfony\Component\Lock\LockFactory; use Symfony\Component\Lock\LockInterface; -use Symfony\Component\Lock\PersistStoreInterface; -use Symfony\Component\Lock\Store\FlockStore; +use Symfony\Component\Lock\PersistingStoreInterface; use Symfony\Component\Lock\Store\StoreFactory; use Symfony\Component\Lock\StoreInterface; -use Symfony\Component\Mailer\Bridge\Amazon\Factory\SesTransportFactory; -use Symfony\Component\Mailer\Bridge\Google\Factory\GmailTransportFactory; -use Symfony\Component\Mailer\Bridge\Mailchimp\Factory\MandrillTransportFactory; -use Symfony\Component\Mailer\Bridge\Mailgun\Factory\MailgunTransportFactory; -use Symfony\Component\Mailer\Bridge\Postmark\Factory\PostmarkTransportFactory; -use Symfony\Component\Mailer\Bridge\Sendgrid\Factory\SendgridTransportFactory; +use Symfony\Component\Mailer\Bridge\Amazon\Transport\SesTransportFactory; +use Symfony\Component\Mailer\Bridge\Google\Transport\GmailTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailchimp\Transport\MandrillTransportFactory; +use Symfony\Component\Mailer\Bridge\Mailgun\Transport\MailgunTransportFactory; +use Symfony\Component\Mailer\Bridge\Postmark\Transport\PostmarkTransportFactory; +use Symfony\Component\Mailer\Bridge\Sendgrid\Transport\SendgridTransportFactory; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; use Symfony\Component\Messenger\MessageBus; use Symfony\Component\Messenger\MessageBusInterface; +use Symfony\Component\Messenger\Transport\AmqpExt\AmqpTransportFactory; +use Symfony\Component\Messenger\Transport\RedisExt\RedisTransportFactory; use Symfony\Component\Messenger\Transport\TransportFactoryInterface; use Symfony\Component\Messenger\Transport\TransportInterface; use Symfony\Component\Mime\MimeTypeGuesserInterface; @@ -133,12 +136,8 @@ use Symfony\Contracts\Translation\LocaleAwareInterface; /** - * FrameworkExtension. - * - * @author Fabien Potencier - * @author Jeremy Mikola - * @author Kévin Dunglas - * @author Grégoire Pineau + * Process the configuration and prepare the dependency injection container with + * parameters and services. */ class FrameworkExtension extends Extension { @@ -148,6 +147,8 @@ class FrameworkExtension extends Extension private $annotationsConfigEnabled = false; private $validatorConfigEnabled = false; private $messengerConfigEnabled = false; + private $mailerConfigEnabled = false; + private $httpClientConfigEnabled = false; /** * Responds to the app.config configuration parameter. @@ -193,7 +194,7 @@ public function load(array $configs, ContainerBuilder $container) // default in the Form and Validator component). If disabled, an identity // translator will be used and everything will still work as expected. if ($this->isConfigEnabled($container, $config['translator']) || $this->isConfigEnabled($container, $config['form']) || $this->isConfigEnabled($container, $config['validation'])) { - if (!class_exists('Symfony\Component\Translation\Translator') && $this->isConfigEnabled($container, $config['translator'])) { + if (!class_exists(Translator::class) && $this->isConfigEnabled($container, $config['translator'])) { throw new LogicException('Translation support cannot be enabled as the Translation component is not installed. Try running "composer require symfony/translation".'); } @@ -209,6 +210,7 @@ public function load(array $configs, ContainerBuilder $container) $container->setParameter('kernel.http_method_override', $config['http_method_override']); $container->setParameter('kernel.trusted_hosts', $config['trusted_hosts']); $container->setParameter('kernel.default_locale', $config['default_locale']); + $container->setParameter('kernel.error_controller', $config['error_controller']); if (!$container->hasParameter('debug.file_link_format')) { if (!$container->hasParameter('templating.helper.code.file_link_format')) { @@ -225,7 +227,7 @@ public function load(array $configs, ContainerBuilder $container) // mark any env vars found in the ide setting as used $container->resolveEnvPlaceholders($ide); - $container->setParameter('templating.helper.code.file_link_format', str_replace('%', '%%', ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')) ?: (isset($links[$ide]) ? $links[$ide] : $ide)); + $container->setParameter('templating.helper.code.file_link_format', str_replace('%', '%%', \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format')) ?: ($links[$ide] ?? $ide)); } $container->setParameter('debug.file_link_format', '%templating.helper.code.file_link_format%'); } @@ -238,9 +240,12 @@ public function load(array $configs, ContainerBuilder $container) } } + // register cache before session so both can share the connection services + $this->registerCacheConfiguration($config['cache'], $container); + if ($this->isConfigEnabled($container, $config['session'])) { if (!\extension_loaded('session')) { - throw new LogicException('Session support cannot be enabled as the session extension is not installed. See https://www.php.net/session.installation for instructions.'); + throw new LogicException('Session support cannot be enabled as the session extension is not installed. See https://php.net/session.installation for instructions.'); } $this->sessionConfigEnabled = true; @@ -260,14 +265,14 @@ public function load(array $configs, ContainerBuilder $container) $this->registerSecurityCsrfConfiguration($config['csrf_protection'], $container, $loader); if ($this->isConfigEnabled($container, $config['form'])) { - if (!class_exists('Symfony\Component\Form\Form')) { + if (!class_exists(\Symfony\Component\Form\Form::class)) { throw new LogicException('Form support cannot be enabled as the Form component is not installed. Try running "composer require symfony/form".'); } $this->formConfigEnabled = true; $this->registerFormConfiguration($config, $container, $loader); - if (class_exists('Symfony\Component\Validator\Validation')) { + if (class_exists(\Symfony\Component\Validator\Validation::class)) { $config['validation']['enabled'] = true; } else { $container->setParameter('validator.translation_domain', 'validators'); @@ -280,7 +285,7 @@ public function load(array $configs, ContainerBuilder $container) } if ($this->isConfigEnabled($container, $config['assets'])) { - if (!class_exists('Symfony\Component\Asset\Package')) { + if (!class_exists(\Symfony\Component\Asset\Package::class)) { throw new LogicException('Asset support cannot be enabled as the Asset component is not installed. Try running "composer require symfony/asset".'); } @@ -288,9 +293,9 @@ public function load(array $configs, ContainerBuilder $container) } if ($this->isConfigEnabled($container, $config['templating'])) { - @trigger_error('Enabling the Templating component is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); + @trigger_error('Enabling the Templating component is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); - if (!class_exists('Symfony\Component\Templating\PhpEngine')) { + if (!class_exists(\Symfony\Component\Templating\PhpEngine::class)) { throw new LogicException('Templating support cannot be enabled as the Templating component is not installed. Try running "composer require symfony/templating".'); } @@ -298,7 +303,7 @@ public function load(array $configs, ContainerBuilder $container) } if ($this->messengerConfigEnabled = $this->isConfigEnabled($container, $config['messenger'])) { - $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['serializer'], $config['validation']); + $this->registerMessengerConfiguration($config['messenger'], $container, $loader, $config['validation']); } else { $container->removeDefinition('console.command.messenger_consume_messages'); $container->removeDefinition('console.command.messenger_debug'); @@ -307,6 +312,25 @@ public function load(array $configs, ContainerBuilder $container) $container->removeDefinition('console.command.messenger_failed_messages_retry'); $container->removeDefinition('console.command.messenger_failed_messages_show'); $container->removeDefinition('console.command.messenger_failed_messages_remove'); + $container->removeDefinition('cache.messenger.restart_workers_signal'); + + if ($container->hasDefinition('messenger.transport.amqp.factory') && class_exists(AmqpTransportFactory::class)) { + $container->getDefinition('messenger.transport.amqp.factory') + ->addTag('messenger.transport_factory'); + } + + if ($container->hasDefinition('messenger.transport.redis.factory') && class_exists(RedisTransportFactory::class)) { + $container->getDefinition('messenger.transport.redis.factory') + ->addTag('messenger.transport_factory'); + } + } + + if ($this->httpClientConfigEnabled = $this->isConfigEnabled($container, $config['http_client'])) { + $this->registerHttpClientConfiguration($config['http_client'], $container, $loader, $config['profiler']); + } + + if ($this->mailerConfigEnabled = $this->isConfigEnabled($container, $config['mailer'])) { + $this->registerMailerConfiguration($config['mailer'], $container, $loader); } $propertyInfoEnabled = $this->isConfigEnabled($container, $config['property_info']); @@ -316,15 +340,15 @@ public function load(array $configs, ContainerBuilder $container) $this->registerFragmentsConfiguration($config['fragments'], $container, $loader); $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']); $this->registerProfilerConfiguration($config['profiler'], $container, $loader); - $this->registerCacheConfiguration($config['cache'], $container); $this->registerWorkflowConfiguration($config['workflows'], $container, $loader); $this->registerDebugConfiguration($config['php_errors'], $container, $loader); $this->registerRouterConfiguration($config['router'], $container, $loader); $this->registerAnnotationsConfiguration($config['annotations'], $container, $loader); $this->registerPropertyAccessConfiguration($config['property_access'], $container, $loader); + $this->registerSecretsConfiguration($config['secrets'], $container, $loader); if ($this->isConfigEnabled($container, $config['serializer'])) { - if (!class_exists('Symfony\Component\Serializer\Serializer')) { + if (!class_exists(\Symfony\Component\Serializer\Serializer::class)) { throw new LogicException('Serializer support cannot be enabled as the Serializer component is not installed. Try running "composer require symfony/serializer-pack".'); } @@ -339,14 +363,6 @@ public function load(array $configs, ContainerBuilder $container) $this->registerLockConfiguration($config['lock'], $container, $loader); } - if ($this->isConfigEnabled($container, $config['http_client'])) { - $this->registerHttpClientConfiguration($config['http_client'], $container, $loader); - } - - if ($this->isConfigEnabled($container, $config['mailer'])) { - $this->registerMailerConfiguration($config['mailer'], $container, $loader); - } - if ($this->isConfigEnabled($container, $config['web_link'])) { if (!class_exists(HttpHeaderSerializer::class)) { throw new LogicException('WebLink support cannot be enabled as the WebLink component is not installed. Try running "composer require symfony/weblink".'); @@ -371,6 +387,8 @@ public function load(array $configs, ContainerBuilder $container) ->addTag('console.command'); $container->registerForAutoconfiguration(ResourceCheckerInterface::class) ->addTag('config_cache.resource_checker'); + $container->registerForAutoconfiguration(EnvVarLoaderInterface::class) + ->addTag('container.env_var_loader'); $container->registerForAutoconfiguration(EnvVarProcessorInterface::class) ->addTag('container.env_var_processor'); $container->registerForAutoconfiguration(ServiceLocator::class) @@ -446,6 +464,19 @@ public function load(array $configs, ContainerBuilder $container) if (!$config['disallow_search_engine_index'] ?? false) { $container->removeDefinition('disallow_search_engine_index_response_listener'); } + + $container->registerForAutoconfiguration(RouteLoaderInterface::class) + ->addTag('routing.route_loader'); + + $container->setParameter('container.behavior_describing_tags', [ + 'annotations.cached_reader', + 'container.do_not_inline', + 'container.service_locator', + 'container.service_subscriber', + 'kernel.event_subscriber', + 'kernel.locale_aware', + 'kernel.reset', + ]); } /** @@ -465,6 +496,10 @@ private function registerFormConfiguration(array $config, ContainerBuilder $cont } if ($this->isConfigEnabled($container, $config['form']['csrf_protection'])) { + if (!$container->hasDefinition('security.csrf.token_generator')) { + throw new \LogicException('To use form CSRF protection, "framework.csrf_protection" must be enabled.'); + } + $loader->load('form_csrf.xml'); $container->setParameter('form.type_extension.csrf.enabled', true); @@ -512,7 +547,7 @@ private function registerFragmentsConfiguration(array $config, ContainerBuilder return; } - if ($container->hasParameter('fragment.renderer.hinclude.global_template') && null !== $container->getParameter('fragment.renderer.hinclude.global_template') && null !== $config['hinclude_default_template']) { + if ($container->hasParameter('fragment.renderer.hinclude.global_template') && '' !== $container->getParameter('fragment.renderer.hinclude.global_template') && null !== $config['hinclude_default_template']) { throw new \LogicException('You cannot set both "templating.hinclude_default_template" and "fragments.hinclude_default_template", please only use "fragments.hinclude_default_template".'); } @@ -553,11 +588,19 @@ private function registerProfilerConfiguration(array $config, ContainerBuilder $ $loader->load('messenger_debug.xml'); } + if ($this->mailerConfigEnabled) { + $loader->load('mailer_debug.xml'); + } + + if ($this->httpClientConfigEnabled) { + $loader->load('http_client_debug.xml'); + } + $container->setParameter('profiler_listener.only_exceptions', $config['only_exceptions']); $container->setParameter('profiler_listener.only_master_requests', $config['only_master_requests']); // Choose storage class based on the DSN - list($class) = explode(':', $config['dsn'], 2); + [$class] = explode(':', $config['dsn'], 2); if ('file' !== $class) { throw new \LogicException(sprintf('Driver "%s" is not supported for the profiler.', $class)); } @@ -680,8 +723,8 @@ private function registerWorkflowConfiguration(array $config, ContainerBuilder $ $markingStoreDefinition = new ChildDefinition('workflow.marking_store.'.$workflow['marking_store']['type']); if ('method' === $workflow['marking_store']['type']) { $markingStoreDefinition->setArguments([ - 'state_machine' === $type, //single state - $workflow['marking_store']['property'], + 'state_machine' === $type, // single state + $workflow['marking_store']['property'] ?? 'marking', ]); } else { foreach ($workflow['marking_store']['arguments'] as $argument) { @@ -858,28 +901,30 @@ private function registerRouterConfiguration(array $config, ContainerBuilder $co $container->setParameter('request_listener.http_port', $config['http_port']); $container->setParameter('request_listener.https_port', $config['https_port']); - if ($this->annotationsConfigEnabled) { - $container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->addArgument(new Reference('annotation_reader')); - - $container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->setArguments([ - new Reference('file_locator'), - new Reference('routing.loader.annotation'), - ]); - - $container->register('routing.loader.annotation.file', AnnotationFileLoader::class) - ->setPublic(false) - ->addTag('routing.loader', ['priority' => -10]) - ->setArguments([ - new Reference('file_locator'), - new Reference('routing.loader.annotation'), - ]); + if (!$this->annotationsConfigEnabled) { + return; } + + $container->register('routing.loader.annotation', AnnotatedRouteControllerLoader::class) + ->setPublic(false) + ->addTag('routing.loader', ['priority' => -10]) + ->addArgument(new Reference('annotation_reader')); + + $container->register('routing.loader.annotation.directory', AnnotationDirectoryLoader::class) + ->setPublic(false) + ->addTag('routing.loader', ['priority' => -10]) + ->setArguments([ + new Reference('file_locator'), + new Reference('routing.loader.annotation'), + ]); + + $container->register('routing.loader.annotation.file', AnnotationFileLoader::class) + ->setPublic(false) + ->addTag('routing.loader', ['priority' => -10]) + ->setArguments([ + new Reference('file_locator'), + new Reference('routing.loader.annotation'), + ]); } private function registerSessionConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) @@ -889,7 +934,7 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c // session storage $container->setAlias('session.storage', $config['storage_id'])->setPrivate(true); $options = ['cache_limiter' => '0']; - foreach (['name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'cookie_samesite', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor'] as $key) { + foreach (['name', 'cookie_lifetime', 'cookie_path', 'cookie_domain', 'cookie_secure', 'cookie_httponly', 'cookie_samesite', 'use_cookies', 'gc_maxlifetime', 'gc_probability', 'gc_divisor', 'sid_length', 'sid_bits_per_character'] as $key) { if (isset($config[$key])) { $options[$key] = $config[$key]; } @@ -907,20 +952,23 @@ private function registerSessionConfiguration(array $config, ContainerBuilder $c // session handler (the internal callback registered with PHP session management) if (null === $config['handler_id']) { - // If the user set a save_path without using a non-default \SessionHandler, it will silently be ignored - if (isset($config['save_path'])) { - throw new LogicException('Session save path is ignored without a handler service'); - } - // Set the handler class to be null $container->getDefinition('session.storage.native')->replaceArgument(1, null); $container->getDefinition('session.storage.php_bridge')->replaceArgument(0, null); + $container->setAlias('session.handler', 'session.handler.native_file')->setPrivate(true); } else { - $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); - } + $container->resolveEnvPlaceholders($config['handler_id'], null, $usedEnvs); + + if ($usedEnvs || preg_match('#^[a-z]++://#', $config['handler_id'])) { + $id = '.cache_connection.'.ContainerBuilder::hash($config['handler_id']); - if (!isset($config['save_path'])) { - $config['save_path'] = ini_get('session.save_path'); + $container->getDefinition('session.abstract_handler') + ->replaceArgument(0, $container->hasDefinition($id) ? new Reference($id) : $config['handler_id']); + + $container->setAlias('session.handler', 'session.abstract_handler')->setPrivate(true); + } else { + $container->setAlias('session.handler', $config['handler_id'])->setPrivate(true); + } } $container->setParameter('session.save_path', $config['save_path']); @@ -1021,8 +1069,6 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co { $loader->load('assets.xml'); - $defaultVersion = null; - if ($config['version_strategy']) { $defaultVersion = new Reference($config['version_strategy']); } else { @@ -1042,7 +1088,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co } else { // let format fallback to main version_format $format = $package['version_format'] ?: $config['version_format']; - $version = isset($package['version']) ? $package['version'] : null; + $version = $package['version'] ?? null; $version = $this->createVersion($container, $version, $format, $package['json_manifest_path'], $name); } @@ -1060,7 +1106,7 @@ private function registerAssetsConfiguration(array $config, ContainerBuilder $co /** * Returns a definition for an asset package. */ - private function createPackageDefinition($basePath, array $baseUrls, Reference $version) + private function createPackageDefinition(?string $basePath, array $baseUrls, Reference $version): Definition { if ($basePath && $baseUrls) { throw new \LogicException('An asset package cannot have base URLs and base paths.'); @@ -1076,7 +1122,7 @@ private function createPackageDefinition($basePath, array $baseUrls, Reference $ return $package; } - private function createVersion(ContainerBuilder $container, $version, $format, $jsonManifestPath, $name) + private function createVersion(ContainerBuilder $container, ?string $version, ?string $format, ?string $jsonManifestPath, string $name): Reference { // Configuration prevents $version and $jsonManifestPath from being set if (null !== $version) { @@ -1118,6 +1164,10 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $translator = $container->findDefinition('translator.default'); $translator->addMethodCall('setFallbackLocales', [$config['fallbacks'] ?: [$defaultLocale]]); + $defaultOptions = $translator->getArgument(4); + $defaultOptions['cache_dir'] = $config['cache_dir']; + $translator->setArgument(4, $defaultOptions); + $container->setParameter('translator.logging', $config['logging']); $container->setParameter('translator.default_path', $config['default_path']); @@ -1125,31 +1175,31 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $dirs = []; $transPaths = []; $nonExistingDirs = []; - if (class_exists('Symfony\Component\Validator\Validation')) { - $r = new \ReflectionClass('Symfony\Component\Validator\Validation'); + if (class_exists(\Symfony\Component\Validator\Validation::class)) { + $r = new \ReflectionClass(\Symfony\Component\Validator\Validation::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (class_exists('Symfony\Component\Form\Form')) { - $r = new \ReflectionClass('Symfony\Component\Form\Form'); + if (class_exists(\Symfony\Component\Form\Form::class)) { + $r = new \ReflectionClass(\Symfony\Component\Form\Form::class); $dirs[] = $transPaths[] = \dirname($r->getFileName()).'/Resources/translations'; } - if (class_exists('Symfony\Component\Security\Core\Exception\AuthenticationException')) { - $r = new \ReflectionClass('Symfony\Component\Security\Core\Exception\AuthenticationException'); + if (class_exists(\Symfony\Component\Security\Core\Exception\AuthenticationException::class)) { + $r = new \ReflectionClass(\Symfony\Component\Security\Core\Exception\AuthenticationException::class); - $dirs[] = $transPaths[] = \dirname(\dirname($r->getFileName())).'/Resources/translations'; + $dirs[] = $transPaths[] = \dirname($r->getFileName(), 2).'/Resources/translations'; } $defaultDir = $container->getParameterBag()->resolveValue($config['default_path']); $rootDir = $container->getParameter('kernel.root_dir'); foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) { - if (is_dir($dir = $bundle['path'].'/Resources/translations')) { + if ($container->fileExists($dir = $bundle['path'].'/Resources/translations') || $container->fileExists($dir = $bundle['path'].'/translations')) { $dirs[] = $dir; } else { $nonExistingDirs[] = $dir; } - if (is_dir($dir = $rootDir.sprintf('/Resources/%s/translations', $name))) { - @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), E_USER_DEPRECATED); + if ($container->fileExists($dir = $rootDir.sprintf('/Resources/%s/translations', $name))) { + @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), \E_USER_DEPRECATED); $dirs[] = $dir; } else { $nonExistingDirs[] = $dir; @@ -1157,10 +1207,10 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder } foreach ($config['paths'] as $dir) { - if (is_dir($dir)) { + if ($container->fileExists($dir)) { $dirs[] = $transPaths[] = $dir; } else { - throw new \UnexpectedValueException(sprintf('%s defined in translator.paths does not exist or is not a directory', $dir)); + throw new \UnexpectedValueException(sprintf('"%s" defined in translator.paths does not exist or is not a directory.', $dir)); } } @@ -1172,15 +1222,17 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder $container->getDefinition('console.command.translation_update')->replaceArgument(6, $transPaths); } - if (is_dir($defaultDir)) { + if (null === $defaultDir) { + // allow null + } elseif ($container->fileExists($defaultDir)) { $dirs[] = $defaultDir; } else { $nonExistingDirs[] = $defaultDir; } - if (is_dir($dir = $rootDir.'/Resources/translations')) { + if ($container->fileExists($dir = $rootDir.'/Resources/translations')) { if ($dir !== $defaultDir) { - @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), E_USER_DEPRECATED); + @trigger_error(sprintf('Translations directory "%s" is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultDir), \E_USER_DEPRECATED); } $dirs[] = $dir; @@ -1191,31 +1243,40 @@ private function registerTranslatorConfiguration(array $config, ContainerBuilder // Register translation resources if ($dirs) { $files = []; - $finder = Finder::create() - ->followLinks() - ->files() - ->filter(function (\SplFileInfo $file) { - return 2 <= substr_count($file->getBasename(), '.') && preg_match('/\.\w+$/', $file->getBasename()); - }) - ->in($dirs) - ->sortByName() - ; - foreach ($finder as $file) { - $fileNameParts = explode('.', basename($file)); - $locale = $fileNameParts[\count($fileNameParts) - 2]; - if (!isset($files[$locale])) { - $files[$locale] = []; - } + foreach ($dirs as $dir) { + $finder = Finder::create() + ->followLinks() + ->files() + ->filter(function (\SplFileInfo $file) { + return 2 <= substr_count($file->getBasename(), '.') && preg_match('/\.\w+$/', $file->getBasename()); + }) + ->in($dir) + ->sortByName() + ; + foreach ($finder as $file) { + $fileNameParts = explode('.', basename($file)); + $locale = $fileNameParts[\count($fileNameParts) - 2]; + if (!isset($files[$locale])) { + $files[$locale] = []; + } - $files[$locale][] = (string) $file; + $files[$locale][] = (string) $file; + } } + $projectDir = $container->getParameter('kernel.project_dir'); + $options = array_merge( $translator->getArgument(4), [ 'resource_files' => $files, - 'scanned_directories' => array_merge($dirs, $nonExistingDirs), + 'scanned_directories' => $scannedDirectories = array_merge($dirs, $nonExistingDirs), + 'cache_vary' => [ + 'scanned_directories' => array_map(static function (string $dir) use ($projectDir): string { + return str_starts_with($dir, $projectDir.'/') ? substr($dir, 1 + \strlen($projectDir)) : $dir; + }, $scannedDirectories), + ], ] ); @@ -1229,7 +1290,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder return; } - if (!class_exists('Symfony\Component\Validator\Validation')) { + if (!class_exists(\Symfony\Component\Validator\Validation::class)) { throw new LogicException('Validation support cannot be enabled as the Validator component is not installed. Try running "composer require symfony/validator".'); } @@ -1243,7 +1304,7 @@ private function registerValidationConfiguration(array $config, ContainerBuilder if (interface_exists(TranslatorInterface::class) && class_exists(LegacyTranslatorProxy::class)) { $calls = $validatorBuilder->getMethodCalls(); - $calls[1] = ['setTranslator', [new Definition(LegacyTranslatorProxy::class, [new Reference('translator')])]]; + $calls[1] = ['setTranslator', [new Definition(LegacyTranslatorProxy::class, [new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])]]; $validatorBuilder->setMethodCalls($calls); } @@ -1278,11 +1339,11 @@ private function registerValidationConfiguration(array $config, ContainerBuilder } if (!$container->getParameter('kernel.debug')) { - $validatorBuilder->addMethodCall('setMetadataCache', [new Reference('validator.mapping.cache.symfony')]); + $validatorBuilder->addMethodCall('setMappingCache', [new Reference('validator.mapping.cache.adapter')]); } $contai 10000 ner->setParameter('validator.auto_mapping', $config['auto_mapping']); - if (!$propertyInfoEnabled || !$config['auto_mapping'] || !class_exists(PropertyInfoLoader::class)) { + if (!$propertyInfoEnabled || !class_exists(PropertyInfoLoader::class)) { $container->removeDefinition('validator.property_info_loader'); } @@ -1299,26 +1360,26 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $files['yaml' === $extension ? 'yml' : $extension][] = $path; }; - if (interface_exists('Symfony\Component\Form\FormInterface')) { - $reflClass = new \ReflectionClass('Symfony\Component\Form\FormInterface'); + if (interface_exists(\Symfony\Component\Form\FormInterface::class)) { + $reflClass = new \ReflectionClass(\Symfony\Component\Form\FormInterface::class); $fileRecorder('xml', \dirname($reflClass->getFileName()).'/Resources/config/validation.xml'); } foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) { - $dirname = $bundle['path']; + $configDir = is_dir($bundle['path'].'/Resources/config') ? $bundle['path'].'/Resources/config' : $bundle['path'].'/config'; if ( - $container->fileExists($file = $dirname.'/Resources/config/validation.yaml', false) || - $container->fileExists($file = $dirname.'/Resources/config/validation.yml', false) + $container->fileExists($file = $configDir.'/validation.yaml', false) || + $container->fileExists($file = $configDir.'/validation.yml', false) ) { $fileRecorder('yml', $file); } - if ($container->fileExists($file = $dirname.'/Resources/config/validation.xml', false)) { + if ($container->fileExists($file = $configDir.'/validation.xml', false)) { $fileRecorder('xml', $file); } - if ($container->fileExists($dir = $dirname.'/Resources/config/validation', '/^$/')) { + if ($container->fileExists($dir = $configDir.'/validation', '/^$/')) { $this->registerMappingFilesFromDir($dir, $fileRecorder); } } @@ -1331,7 +1392,7 @@ private function registerValidatorMapping(ContainerBuilder $container, array $co $this->registerMappingFilesFromConfig($container, $config, $fileRecorder); } - private function registerMappingFilesFromDir($dir, callable $fileRecorder) + private function registerMappingFilesFromDir(string $dir, callable $fileRecorder) { foreach (Finder::create()->followLinks()->files()->in($dir)->name('/\.(xml|ya?ml)$/')->sortByName() as $file) { $fileRecorder($file->getExtension(), $file->getRealPath()); @@ -1355,13 +1416,13 @@ private function registerMappingFilesFromConfig(ContainerBuilder $container, arr } } - private function registerAnnotationsConfiguration(array $config, ContainerBuilder $container, $loader) + private function registerAnnotationsConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader) { if (!$this->annotationsConfigEnabled) { return; } - if (!class_exists('Doctrine\Common\Annotations\Annotation')) { + if (!class_exists(\Doctrine\Common\Annotations\Annotation::class)) { throw new LogicException('Annotations cannot be enabled as the Doctrine Annotation library is not installed.'); } @@ -1373,14 +1434,20 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } if ('none' !== $config['cache']) { - if (!class_exists('Doctrine\Common\Cache\CacheProvider')) { + if (class_exists(PsrCachedReader::class)) { + $container + ->getDefinition('annotations.cached_reader') + ->setClass(PsrCachedReader::class) + ->replaceArgument(1, new Definition(ArrayAdapter::class)) + ; + } elseif (!class_exists(\Doctrine\Common\Cache\CacheProvider::class)) { throw new LogicException('Annotations cannot be enabled as the Doctrine Cache library is not installed.'); } $cacheService = $config['cache']; if ('php_array' === $config['cache']) { - $cacheService = 'annotations.cache'; + $cacheService = class_exists(PsrCachedReader::class) ? 'annotations.cache_adapter' : 'annotations.cache'; // Enable warmer only if PHP array is used for cache $definition = $container->findDefinition('annotations.cache_warmer'); @@ -1393,19 +1460,18 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde } $container - ->getDefinition('annotations.filesystem_cache') - ->replaceArgument(0, $cacheDir) + ->getDefinition('annotations.filesystem_cache_adapter') + ->replaceArgument(2, $cacheDir) ; - $cacheService = 'annotations.filesystem_cache'; + $cacheService = class_exists(PsrCachedReader::class) ? 'annotations.filesystem_cache_adapter' : 'annotations.filesystem_cache'; } $container ->getDefinition('annotations.cached_reader') ->replaceArgument(2, $config['debug']) - // temporary property to lazy-reference the cache provider without using it until AddAnnotationsCachedReaderPass runs - ->setProperty('cacheProviderBackup', new ServiceClosureArgument(new Reference($cacheService))) - ->addTag('annotations.cached_reader') + // reference the cache provider without using it until AddAnnotationsCachedReaderPass runs + ->addArgument(new ServiceClosureArgument(new Reference($cacheService))) ; $container->setAlias('annotation_reader', 'annotations.cached_reader')->setPrivate(true); @@ -1417,7 +1483,7 @@ private function registerAnnotationsConfiguration(array $config, ContainerBuilde private function registerPropertyAccessConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { - if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) { + if (!class_exists(PropertyAccessor::class)) { return; } @@ -1431,13 +1497,47 @@ private function registerPropertyAccessConfiguration(array $config, ContainerBui ; } + private function registerSecretsConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + { + if (!$this->isConfigEnabled($container, $config)) { + $container->removeDefinition('console.command.secrets_set'); + $container->removeDefinition('console.command.secrets_list'); + $container->removeDefinition('console.command.secrets_remove'); + $container->removeDefinition('console.command.secrets_generate_key'); + $container->removeDefinition('console.command.secrets_decrypt_to_local'); + $container->removeDefinition('console.command.secrets_encrypt_from_local'); + + return; + } + + $loader->load('secrets.xml'); + + $container->getDefinition('secrets.vault')->replaceArgument(0, $config['vault_directory']); + + if ($config['local_dotenv_file']) { + $container->getDefinition('secrets.local_vault')->replaceArgument(0, $config['local_dotenv_file']); + } else { + $container->removeDefinition('secrets.local_vault'); + } + + if ($config['decryption_env_var']) { + if (!preg_match('/^(?:\w*+:)*+\w++$/', $config['decryption_env_var'])) { + throw new InvalidArgumentException(sprintf('Invalid value "%s" set as "decryption_env_var": only "word" characters are allowed.', $config['decryption_env_var'])); + } + + $container->getDefinition('secrets.vault')->replaceArgument(1, "%env({$config['decryption_env_var']})%"); + } else { + $container->getDefinition('secrets.vault')->replaceArgument(1, null); + } + } + private function registerSecurityCsrfConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) { if (!$this->isConfigEnabled($container, $config)) { return; } - if (!class_exists('Symfony\Component\Security\Csrf\CsrfToken')) { + if (!class_exists(\Symfony\Component\Security\Csrf\CsrfToken::class)) { throw new LogicException('CSRF support cannot be enabled as the Security CSRF component is not installed. Try running "composer require symfony/security-csrf".'); } @@ -1468,7 +1568,7 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder $chainLoader = $container->getDefinition('serializer.mapping.chain_loader'); - if (!class_exists('Symfony\Component\PropertyAccess\PropertyAccessor')) { + if (!class_exists(PropertyAccessor::class)) { $container->removeAlias('serializer.property_accessor'); $container->removeDefinition('serializer.normalizer.object'); } @@ -1499,20 +1599,20 @@ private function registerSerializerConfiguration(array $config, ContainerBuilder }; foreach ($container->getParameter('kernel.bundles_metadata') as $bundle) { - $dirname = $bundle['path']; + $configDir = is_dir($bundle['path'].'/Resources/config') ? $bundle['path'].'/Resources/config' : $bundle['path'].'/config'; - if ($container->fileExists($file = $dirname.'/Resources/config/serialization.xml', false)) { + if ($container->fileExists($file = $configDir.'/serialization.xml', false)) { $fileRecorder('xml', $file); } if ( - $container->fileExists($file = $dirname.'/Resources/config/serialization.yaml', false) || - $container->fileExists($file = $dirname.'/Resources/config/serialization.yml', false) + $container->fileExists($file = $configDir.'/serialization.yaml', false) || + $container->fileExists($file = $configDir.'/serialization.yml', false) ) { $fileRecorder('yml', $file); } - if ($container->fileExists($dir = $dirname.'/Resources/config/serialization', '/^$/')) { + if ($container->fileExists($dir = $configDir.'/serialization', '/^$/')) { $this->registerMappingFilesFromDir($dir, $fileRecorder); } } @@ -1557,7 +1657,7 @@ private function registerPropertyInfoConfiguration(ContainerBuilder $container, $loader->load('property_info.xml'); - if (interface_exists('phpDocumentor\Reflection\DocBlockFactoryInterface')) { + if (interface_exists(\phpDocumentor\Reflection\DocBlockFactoryInterface::class)) { $definition = $container->register('property_info.php_doc_extractor', 'Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor'); $definition->setPrivate(true); $definition->addTag('property_info.description_extractor', ['priority' => -1000]); @@ -1580,44 +1680,15 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont // Generate stores $storeDefinitions = []; - foreach ($resourceStores as $storeDsn) { - $storeDsn = $container->resolveEnvPlaceholders($storeDsn, null, $usedEnvs); - switch (true) { - case 'flock' === $storeDsn: - $storeDefinition = new Reference('lock.store.flock'); - break; - case 0 === strpos($storeDsn, 'flock://'): - $flockPath = substr($storeDsn, 8); - - $storeDefinitionId = '.lock.flock.store.'.$container->hash($storeDsn); - $container->register($storeDefinitionId, FlockStore::class)->addArgument($flockPath); - - $storeDefinition = new Reference($storeDefinitionId); - break; - case 'semaphore' === $storeDsn: - $storeDefinition = new Reference('lock.store.semaphore'); - break; - case $usedEnvs || preg_match('#^[a-z]++://#', $storeDsn): - if (!$container->hasDefinition($connectionDefinitionId = '.lock_connection.'.$container->hash($storeDsn))) { - $connectionDefinition = new Definition(\stdClass::class); - $connectionDefinition->setPublic(false); - $connectionDefinition->setFactory([AbstractAdapter::class, 'createConnection']); - $connectionDefinition->setArguments([$storeDsn, ['lazy' => true]]); - $container->setDefinition($connectionDefinitionId, $connectionDefinition); - } - - $storeDefinition = new Definition(PersistStoreInterface::class); - $storeDefinition->setPublic(false); - $storeDefinition->setFactory([StoreFactory::class, 'createStore']); - $storeDefinition->setArguments([new Reference($connectionDefinitionId)]); + foreach ($resourceStores as $resourceStore) { + $storeDsn = $container->resolveEnvPlaceholders($resourceStore, null, $usedEnvs); + $storeDefinition = new Definition(interface_exists(StoreInterface::class) ? StoreInterface::class : PersistingStoreInterface::class); + $storeDefinition->setFactory([StoreFactory::class, 'createStore']); + $storeDefinition->setArguments([$resourceStore]); - $container->setDefinition($storeDefinitionId = '.lock.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition); + $container->setDefinition($storeDefinitionId = '.lock.'.$resourceName.'.store.'.$container->hash($storeDsn), $storeDefinition); - $storeDefinition = new Reference($storeDefinitionId); - break; - default: - throw new InvalidArgumentException(sprintf('Lock store DSN "%s" is not valid in resource "%s"', $storeDsn, $resourceName)); - } + $storeDefinition = new Reference($storeDefinitionId); $storeDefinitions[] = $storeDefinition; } @@ -1649,13 +1720,13 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont $container->setAlias('lock.factory', new Alias('lock.'.$resourceName.'.factory', false)); $container->setAlias('lock', new Alias('lock.'.$resourceName, false)); $container->setAlias(StoreInterface::class, new Alias('lock.store', false)); - $container->setAlias(PersistStoreInterface::class, new Alias('lock.store', false)); + $container->setAlias(PersistingStoreInterface::class, new Alias('lock.store', false)); $container->setAlias(Factory::class, new Alias('lock.factory', false)); $container->setAlias(LockFactory::class, new Alias('lock.factory', false)); $container->setAlias(LockInterface::class, new Alias('lock', false)); } else { $container->registerAliasForArgument('lock.'.$resourceName.'.store', StoreInterface::class, $resourceName.'.lock.store'); - $container->registerAliasForArgument('lock.'.$resourceName.'.store', PersistStoreInterface::class, $resourceName.'.lock.store'); + $container->registerAliasForArgument('lock.'.$resourceName.'.store', PersistingStoreInterface::class, $resourceName.'.lock.store'); $container->registerAliasForArgument('lock.'.$resourceName.'.factory', Factory::class, $resourceName.'.lock.factory'); $container->registerAliasForArgument('lock.'.$resourceName.'.factory', LockFactory::class, $resourceName.'.lock.factory'); $container->registerAliasForArgument('lock.'.$resourceName, LockInterface::class, $resourceName.'.lock'); @@ -1663,7 +1734,7 @@ private function registerLockConfiguration(array $config, ContainerBuilder $cont } } - private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $serializerConfig, array $validationConfig) + private function registerMessengerConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $validationConfig) { if (!interface_exists(MessageBusInterface::class)) { throw new LogicException('Messenger support cannot be enabled as the Messenger component is not installed. Try running "composer require symfony/messenger".'); @@ -1671,6 +1742,14 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $loader->load('messenger.xml'); + if (class_exists(AmqpTransportFactory::class)) { + $container->getDefinition('messenger.transport.amqp.factory')->addTag('messenger.transport_factory'); + } + + if (class_exists(RedisTransportFactory::class)) { + $container->getDefinition('messenger.transport.redis.factory')->addTag('messenger.transport_factory'); + } + if (null === $config['default_bus'] && 1 === \count($config['buses'])) { $config['default_bus'] = key($config['buses']); } @@ -1678,6 +1757,7 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $defaultMiddleware = [ 'before' => [ ['id' => 'add_bus_name_stamp_middleware'], + ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ], @@ -1742,7 +1822,7 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder $transportDefinition = (new Definition(TransportInterface::class)) ->setFactory([new Reference('messenger.transport_factory'), 'createTransport']) - ->setArguments([$transport['dsn'], $transport['options'], new Reference($serializerId)]) + ->setArguments([$transport['dsn'], $transport['options'] + ['transport_name' => $name], new Reference($serializerId)]) ->addTag('messenger.receiver', ['alias' => $name]) ; $container->setDefinition($transportId = 'messenger.transport.'.$name, $transportDefinition); @@ -1764,6 +1844,16 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder } } + $senderReferences = []; + // alias => service_id + foreach ($senderAliases as $alias => $serviceId) { + $senderReferences[$alias] = new Reference($serviceId); + } + // service_id => service_id + foreach ($senderAliases as $serviceId) { + $senderReferences[$serviceId] = new Reference($serviceId); + } + $messageToSendersMapping = []; foreach ($config['routing'] as $message => $messageConfiguration) { if ('*' !== $message && !class_exists($message) && !interface_exists($message, false)) { @@ -1772,33 +1862,37 @@ private function registerMessengerConfiguration(array $config, ContainerBuilder // make sure senderAliases contains all senders foreach ($messageConfiguration['senders'] as $sender) { - if (!isset($senderAliases[$sender])) { - $senderAliases[$sender] = $sender; + if (!isset($senderReferences[$sender])) { + throw new LogicException(sprintf('Invalid Messenger routing configuration: the "%s" class is being routed to a sender called "%s". This is not a valid transport or service id.', $message, $sender)); } } $messageToSendersMapping[$message] = $messageConfiguration['senders']; } - $senderReferences = []; - foreach ($senderAliases as $alias => $serviceId) { - $senderReferences[$alias] = new Reference($serviceId); - } + $sendersServiceLocator = ServiceLocatorTagPass::register($container, $senderReferences); $container->getDefinition('messenger.senders_locator') ->replaceArgument(0, $messageToSendersMapping) - ->replaceArgument(1, ServiceLocatorTagPass::register($container, $senderReferences)) + ->replaceArgument(1, $sendersServiceLocator) + ; + + $container->getDefinition('messenger.retry.send_failed_message_for_retry_listener') + ->replaceArgument(0, $sendersServiceLocator) ; $container->getDefinition('messenger.retry_strategy_locator') ->replaceArgument(0, $transportRetryReferences); if ($config['failure_transport']) { + if (!isset($senderReferences[$config['failure_transport']])) { + throw new LogicException(sprintf('Invalid Messenger configuration: the failure transport "%s" is not a valid transport or service id.', $config['failure_transport'])); + } + $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener') - ->replaceArgument(1, $config['failure_transport']); + ->replaceArgument(0, $senderReferences[$config['failure_transport']]); $container->getDefinition('console.command.messenger_failed_messages_retry') - ->replaceArgument(0, $config['failure_transport']) - ->replaceArgument(4, $transportRetryReferences[$config['failure_transport']] ?? null); + ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_show') ->replaceArgument(0, $config['failure_transport']); $container->getDefinition('console.command.messenger_failed_messages_remove') @@ -1870,6 +1964,13 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con ->setPublic($pool['public']) ; + if (method_exists(TagAwareAdapter::class, 'setLogger')) { + $container + ->getDefinition($name) + ->addMethodCall('setLogger', [new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]) + ->addTag('monolog.logger', ['channel' => 'cache']); + } + $pool['name'] = $name; $pool['public'] = false; $name = '.'.$name.'.inner'; @@ -1901,7 +2002,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con if (!$container->getParameter('kernel.debug')) { $propertyAccessDefinition->setFactory([PropertyAccessor::class, 'createCache']); - $propertyAccessDefinition->setArguments([null, 0, $version, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]); + $propertyAccessDefinition->setArguments(['', 0, $version, new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]); $propertyAccessDefinition->addTag('cache.pool', ['clearer' => 'cache.system_clearer']); $propertyAccessDefinition->addTag('monolog.logger', ['channel' => 'cache']); } else { @@ -1911,7 +2012,7 @@ private function registerCacheConfiguration(array $config, ContainerBuilder $con } } - private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader) + private function registerHttpClientConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader, array $profilerConfig) { $loader->load('http_client.xml'); @@ -1926,6 +2027,8 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder $container->removeDefinition(HttpClient::class); } + $httpClientId = $this->isConfigEnabled($container, $profilerConfig) ? '.debug.http_client.inner' : 'http_client'; + foreach ($config['scoped_clients'] as $name => $scopeConfig) { if ('http_client' === $name) { throw new InvalidArgumentException(sprintf('Invalid scope name: "%s" is reserved.', $name)); @@ -1935,12 +2038,19 @@ private function registerHttpClientConfiguration(array $config, ContainerBuilder unset($scopeConfig['scope']); if (null === $scope) { + $baseUri = $scopeConfig['base_uri']; + unset($scopeConfig['base_uri']); + $container->register($name, ScopingHttpClient::class) ->setFactory([ScopingHttpClient::class, 'forBaseUri']) - ->setArguments([new Reference('http_client'), $scopeConfig['base_uri'], $scopeConfig]); + ->setArguments([new Reference($httpClientId), $baseUri, $scopeConfig]) + ->addTag('http_client.client') + ; } else { $container->register($name, ScopingHttpClient::class) - ->setArguments([new Reference('http_client'), [$scope => $scopeConfig], $scope]); + ->setArguments([new Reference($httpClientId), [$scope => $scopeConfig], $scope]) + ->addTag('http_client.client') + ; } $container->registerAliasForArgument($name, HttpClientInterface::class); @@ -1962,7 +2072,12 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co $loader->load('mailer.xml'); $loader->load('mailer_transports.xml'); - $container->getDefinition('mailer.default_transport')->setArgument(0, $config['dsn']); + if (!\count($config['transports']) && null === $config['dsn']) { + $config['dsn'] = 'smtp://null'; + } + $transports = $config['dsn'] ? ['main' => $config['dsn']] : $config['transports']; + $container->getDefinition('mailer.transports')->setArgument(0, $transports); + $container->getDefinition('mailer.default_transport')->setArgument(0, current($transports)); $classToServices = [ SesTransportFactory::class => 'mailer.transport_factory.amazon', @@ -1988,9 +2103,7 @@ private function registerMailerConfiguration(array $config, ContainerBuilder $co } /** - * Returns the base path for the XSD files. - * - * @return string The XSD base path + * {@inheritdoc} */ public function getXsdValidationBasePath() { diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php index 169c03277970f..ef146419013bc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/ResolveControllerNameSubscriber.php @@ -32,7 +32,7 @@ class ResolveControllerNameSubscriber implements EventSubscriberInterface public function __construct(ControllerNameParser $parser, bool $triggerDeprecation = true) { if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1.', __CLASS__), \E_USER_DEPRECATED); } $this->parser = $parser; @@ -48,18 +48,18 @@ public function resolveControllerName(...$args) public function __call(string $method, array $args) { - if ('onKernelRequest' !== $method && 'onKernelRequest' !== strtolower($method)) { - throw new \Error(sprintf('Error: Call to undefined method %s::%s()', \get_class($this), $method)); + if ('onKernelRequest' !== $method && 'onkernelrequest' !== strtolower($method)) { + throw new \Error(sprintf('Error: Call to undefined method "%s::%s()".', static::class, $method)); } $event = $args[0]; $controller = $event->getRequest()->attributes->get('_controller'); - if (\is_string($controller) && false === strpos($controller, '::') && 2 === substr_count($controller, ':')) { + if (\is_string($controller) && !str_contains($controller, '::') && 2 === substr_count($controller, ':')) { // controller in the a:b:c notation then $event->getRequest()->attributes->set('_controller', $parsedNotation = $this->parser->parse($controller, false)); - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $controller, $parsedNotation), E_USER_DEPRECATED); + @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $controller, $parsedNotation), \E_USER_DEPRECATED); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php b/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php index 692a878a9d507..231329c0bf07c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php +++ b/src/Symfony/Bundle/FrameworkBundle/EventListener/SuggestMissingPackageSubscriber.php @@ -39,7 +39,7 @@ final class SuggestMissingPackageSubscriber implements EventSubscriberInterface '_default' => ['MakerBundle', 'symfony/maker-bundle --dev'], ], 'server' => [ - 'dump' => ['VarDumper Component', 'symfony/var-dumper --dev'], + 'dump' => ['Debug Bundle', 'symfony/debug-bundle --dev'], '_default' => ['WebServerBundle', 'symfony/web-server-bundle --dev'], ], ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php index b6ae6b7ecf22a..f5c52cf19749d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php @@ -18,24 +18,32 @@ use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\DataCollectorTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\LoggingTranslatorPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\ProfilerPass; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SessionPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TemplatingPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass; +use Symfony\Component\Cache\Adapter\ApcuAdapter; +use Symfony\Component\Cache\Adapter\ArrayAdapter; +use Symfony\Component\Cache\Adapter\ChainAdapter; +use Symfony\Component\Cache\Adapter\PhpArrayAdapter; +use Symfony\Component\Cache\Adapter\PhpFilesAdapter; use Symfony\Component\Cache\DependencyInjection\CacheCollectorPass; use Symfony\Component\Cache\DependencyInjection\CachePoolClearerPass; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Cache\DependencyInjection\CachePoolPrunerPass; use Symfony\Component\Config\Resource\ClassExistenceResource; use Symfony\Component\Console\DependencyInjection\AddConsoleCommandPass; +use Symfony\Component\Debug\ErrorHandler as LegacyErrorHandler; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\Compiler\RegisterReverseContainerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Dotenv\Dotenv; use Symfony\Component\ErrorHandler\ErrorHandler; -use Symfony\Component\ErrorRenderer\DependencyInjection\ErrorRendererPass; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\Form\DependencyInjection\FormPass; +use Symfony\Component\HttpClient\DependencyInjection\HttpClientPass; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\HttpKernel\DependencyInjection\ControllerArgumentValueResolverPass; @@ -58,6 +66,19 @@ use Symfony\Component\Validator\DependencyInjection\AddAutoMappingConfigurationPass; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\DependencyInjection\AddValidatorInitializersPass; +use Symfony\Component\VarExporter\Internal\Hydrator; +use Symfony\Component\VarExporter\Internal\Registry; + +// Help opcache.preload discover always-needed symbols +class_exists(ApcuAdapter::class); +class_exists(ArrayAdapter::class); +class_exists(ChainAdapter::class); +class_exists(PhpArrayAdapter::class); +class_exists(PhpFilesAdapter::class); +class_exists(Dotenv::class); +class_exists(ErrorHandler::class); +class_exists(Hydrator::class); +class_exists(Registry::class); /** * Bundle. @@ -69,6 +90,9 @@ class FrameworkBundle extends Bundle public function boot() { ErrorHandler::register(null, false)->throwAt($this->container->getParameter('debug.error_handler.throw_at'), true); + if (class_exists(LegacyErrorHandler::class, false)) { + LegacyErrorHandler::register(null, false)->throwAt($this->container->getParameter('debug.error_handler.throw_at'), true); + } if ($this->container->getParameter('kernel.http_method_override')) { Request::enableHttpMethodParameterOverride(); @@ -91,11 +115,11 @@ public function build(ContainerBuilder $container) KernelEvents::FINISH_REQUEST, ]; - $container->addCompilerPass(new ErrorRendererPass()); $container->addCompilerPass(new LoggerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterControllerArgumentLocatorsPass()); $container->addCompilerPass(new RemoveEmptyControllerArgumentLocatorsPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RoutingResolverPass()); + $container->addCompilerPass(new DataCollectorTranslatorPass()); $container->addCompilerPass(new ProfilerPass()); // must be registered before removing private services as some might be listeners/subscribers // but as late as possible to get resolved parameters @@ -116,22 +140,23 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new FragmentRendererPass()); $this->addCompilerPassIfExists($container, SerializerPass::class); $this->addCompilerPassIfExists($container, PropertyInfoPass::class); - $container->addCompilerPass(new DataCollectorTranslatorPass()); $container->addCompilerPass(new ControllerArgumentValueResolverPass()); $container->addCompilerPass(new CachePoolPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 32); $container->addCompilerPass(new CachePoolClearerPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new CachePoolPrunerPass(), PassConfig::TYPE_AFTER_REMOVING); $this->addCompilerPassIfExists($container, FormPass::class); $container->addCompilerPass(new WorkflowGuardListenerPass()); - $container->addCompilerPass(new ResettableServicePass()); + $container->addCompilerPass(new ResettableServicePass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32); $container->addCompilerPass(new RegisterLocaleAwareServicesPass()); $container->addCompilerPass(new TestServiceContainerWeakRefPass(), PassConfig::TYPE_BEFORE_REMOVING, -32); $container->addCompilerPass(new TestServiceContainerRealRefPass(), PassConfig::TYPE_AFTER_REMOVING); $this->addCompilerPassIfExists($container, AddMimeTypeGuesserPass::class); $this->addCompilerPassIfExists($container, MessengerPass::class); + $this->addCompilerPassIfExists($container, HttpClientPass::class); $this->addCompilerPassIfExists($container, AddAutoMappingConfigurationPass::class); $container->addCompilerPass(new RegisterReverseContainerPass(true)); $container->addCompilerPass(new RegisterReverseContainerPass(false), PassConfig::TYPE_AFTER_REMOVING); + $container->addCompilerPass(new SessionPass()); if ($container->getParameter('kernel.debug')) { $container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2); @@ -141,7 +166,7 @@ public function build(ContainerBuilder $container) } } - private function addCompilerPassIfExists(ContainerBuilder $container, $class, $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, $priority = 0) + private function addCompilerPassIfExists(ContainerBuilder $container, string $class, string $type = PassConfig::TYPE_BEFORE_OPTIMIZATION, int $priority = 0) { $container->addResource(new ClassExistenceResource($class)); diff --git a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php index e4a630fb48e72..03cd73b4f8994 100644 --- a/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php +++ b/src/Symfony/Bundle/FrameworkBundle/HttpCache/HttpCache.php @@ -29,23 +29,28 @@ class HttpCache extends BaseHttpCache protected $kernel; /** - * @param KernelInterface $kernel A KernelInterface instance - * @param string $cacheDir The cache directory (default used if null) + * @param string $cacheDir The cache directory (default used if null) */ public function __construct(KernelInterface $kernel, string $cacheDir = null) { $this->kernel = $kernel; $this->cacheDir = $cacheDir; - parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge(['debug' => $kernel->isDebug()], $this->getOptions())); + $isDebug = $kernel->isDebug(); + $options = ['debug' => $isDebug]; + + if ($isDebug) { + $options['stale_if_error'] = 0; + } + + parent::__construct($kernel, $this->createStore(), $this->createSurrogate(), array_merge($options, $this->getOptions())); } /** * Forwards the Request to the backend and returns the Response. * - * @param Request $request A Request instance - * @param bool $raw Whether to catch exceptions or not - * @param Response $entry A Response instance (the stale entry if present, null otherwise) + * @param bool $raw Whether to catch exceptions or not + * @param Response $entry A Response instance (the stale entry if present, null otherwise) * * @return Response A Response instance */ diff --git a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php index 3919737e44dac..6f4f3a7dec17b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Kernel/MicroKernelTrait.php @@ -29,8 +29,6 @@ trait MicroKernelTrait * * $routes->import('config/routing.yml'); * $routes->add('/admin', 'App\Controller\AdminController::dashboard', 'admin_dashboard'); - * - * @param RouteCollectionBuilder $routes */ abstract protected function configureRoutes(RouteCollectionBuilder $routes); @@ -39,22 +37,19 @@ abstract protected function configureRoutes(RouteCollectionBuilder $routes); * * You can register extensions: * - * $c->loadFromExtension('framework', [ + * $container->loadFromExtension('framework', [ * 'secret' => '%secret%' * ]); * * Or services: * - * $c->register('halloween', 'FooBundle\HalloweenProvider'); + * $container->register('halloween', 'FooBundle\HalloweenProvider'); * * Or parameters: * - * $c->setParameter('halloween', 'lot of fun'); - * - * @param ContainerBuilder $c - * @param LoaderInterface $loader + * $container->setParameter('halloween', 'lot of fun'); */ - abstract protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader); + abstract protected function configureContainer(ContainerBuilder $container, LoaderInterface $loader); /** * {@inheritdoc} @@ -69,14 +64,20 @@ public function registerContainerConfiguration(LoaderInterface $loader) ], ]); - if ($this instanceof EventSubscriberInterface) { + if (!$container->hasDefinition('kernel')) { $container->register('kernel', static::class) ->setSynthetic(true) ->setPublic(true) - ->addTag('kernel.event_subscriber') ; } + $kernelDefinition = $container->getDefinition('kernel'); + $kernelDefinition->addTag('routing.route_loader'); + + if ($this instanceof EventSubscriberInterface) { + $kernelDefinition->addTag('kernel.event_subscriber'); + } + $this->configureContainer($container, $loader); $container->addObjectResource($this); diff --git a/src/Symfony/Bundle/FrameworkBundle/LICENSE b/src/Symfony/Bundle/FrameworkBundle/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/LICENSE +++ b/src/Symfony/Bundle/FrameworkBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/FrameworkBundle/README.md b/src/Symfony/Bundle/FrameworkBundle/README.md index 9280d874391d0..76c7700fa03af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/README.md +++ b/src/Symfony/Bundle/FrameworkBundle/README.md @@ -1,10 +1,13 @@ FrameworkBundle =============== +FrameworkBundle provides a tight integration between Symfony components and the +Symfony full-stack framework. + Resources --------- - * [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) + * [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/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php new file mode 100644 index 0000000000000..024ba08d9551b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php @@ -0,0 +1,23 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +if ('cli' !== \PHP_SAPI) { + throw new Exception('This script must be run from the command line.'); +} + +require dirname(__DIR__, 6).'/vendor/autoload.php'; + +use Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\UnusedTagsPassUtils; + +$target = dirname(__DIR__, 2).'/DependencyInjection/Compiler/UnusedTagsPass.php'; +$contents = file_get_contents($target); +$contents = preg_replace('{private \$knownTags = \[(.+?)\];}sm', "private \$knownTags = [\n '".implode("',\n '", UnusedTagsPassUtils::getDefinedTags())."',\n ];", $contents); +file_put_contents($target, $contents); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml index 05dffbc29755f..1fb375ea3c472 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/annotations.xml @@ -24,30 +24,44 @@ - + + + + + + + - + + + 0 + + + + %kernel.cache_dir%/annotations.php - #^Symfony\\(?:Component\\HttpKernel\\|Bundle\\FrameworkBundle\\Controller\\(?!AbstractController$|Controller$))# + #^Symfony\\(?:Component\\HttpKernel\\|Bundle\\FrameworkBundle\\Controller\\(?!.*Controller$))# %kernel.debug% + + + %kernel.cache_dir%/annotations.php + + + + - - - - %kernel.cache_dir%/annotations.php - - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml index ebd7d6ce46a6d..7276892940acb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/console.xml @@ -70,6 +70,10 @@ + + + + null @@ -82,15 +86,11 @@ - + + - - - - - @@ -116,10 +116,9 @@ - + - @@ -127,14 +126,14 @@ - + - + @@ -194,5 +193,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml index f95b218d52ded..786158dd899e1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/debug_prod.xml @@ -21,8 +21,6 @@ %kernel.debug% %kernel.debug% - %kernel.charset% - diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml index f6b5f241ea9eb..4d2423feeeede 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/error_renderer.xml @@ -5,33 +5,27 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - - - - - - - %kernel.debug% + + + + + + %kernel.debug% + + %kernel.charset% - %debug.file_link_format% + + %kernel.project_dir% + + + + + + + - - - %kernel.debug% - - - - - - %kernel.debug% - %kernel.charset% - - - - - %kernel.debug% - %kernel.charset% - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml index 17598fa95815c..05a58c4c4cd2c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/form.xml @@ -69,6 +69,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml index a3f0884365b0a..10256b69d5e96 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client.xml @@ -7,6 +7,7 @@ + @@ -25,8 +26,8 @@ - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml new file mode 100644 index 0000000000000..6d6ae4b729093 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/http_client_debug.xml @@ -0,0 +1,12 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml index cdefecd176fa2..a82003c004a15 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/lock.xml @@ -7,16 +7,22 @@ - + + The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. + - + + The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. + + The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. + The "%service_id%" service is deprecated since Symfony 4.4 and will be removed in 5.0. diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml index becf0d1b71606..12d40229932cd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer.xml @@ -6,12 +6,18 @@ - + + + + + + + @@ -23,7 +29,7 @@ - + @@ -32,5 +38,10 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/R 10000 esources/config/mailer_debug.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml new file mode 100644 index 0000000000000..17e1a6ed54ad9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_debug.xml @@ -0,0 +1,13 @@ + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml index bddcc67f01074..d478942a0c3f0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mailer_transports.xml @@ -11,27 +11,27 @@ - + - + - + - + - + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml index b329aa075cf3f..839aba901b915 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/messenger.xml @@ -10,7 +10,7 @@ - + @@ -48,6 +48,8 @@ + + @@ -55,7 +57,7 @@ - + @@ -65,13 +67,9 @@ - - - + - - - + @@ -84,7 +82,7 @@ - + @@ -96,15 +94,37 @@ - + + + + + + + + + - - + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml index d4c1eb15b9088..e91705d1c19ed 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/mime_type.xml @@ -12,5 +12,7 @@ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml index 54e16f5b4bbbf..b85e9fa71d1cc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing.xml @@ -41,8 +41,18 @@ - + The "%service_id%" service is deprecated since Symfony 4.4, use "routing.loader.container" instead. + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml new file mode 100644 index 0000000000000..13a9cc4076c79 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/routing/errors.xml @@ -0,0 +1,12 @@ + + + + + + error_controller::preview + html + \d+ + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd index 532b9339e1e30..0d5e45f9c2b7e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/schema/symfony-1.0.xsd @@ -41,6 +41,7 @@ + @@ -186,6 +187,7 @@ + @@ -265,7 +267,14 @@ - + + + + + + + + @@ -346,7 +355,7 @@ - + @@ -373,6 +382,7 @@ + @@ -423,6 +433,8 @@ + + @@ -453,12 +465,21 @@ + + + + + + + + + @@ -494,6 +515,7 @@ + @@ -556,14 +578,19 @@ + + + + + - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml new file mode 100644 index 0000000000000..65fd1073fd46f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/secrets.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml index 23da8b07bcb04..19defd885259c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/serializer.xml @@ -6,7 +6,6 @@ %kernel.cache_dir%/serialization.php - @@ -38,6 +37,11 @@ + + + + + @@ -56,7 +60,13 @@ - + + + + + %kernel.debug% + + @@ -86,7 +96,6 @@ - null @@ -139,5 +148,26 @@ + + + + + + + + + + + + + + + + + + %kernel.debug% + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml index 10e641c83dd17..5d8508f97dbe9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/services.xml @@ -23,10 +23,6 @@ kernel.view kernel.exception kernel.terminate - security.authentication.success - security.authentication.failure - security.interactive_login - security.switch_user workflow.guard workflow.leave workflow.transition @@ -91,6 +87,7 @@ %kernel.root_dir% + false @@ -124,5 +121,11 @@ + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml index 020e29e7211f6..c9b17f311f576 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/session.xml @@ -13,8 +13,6 @@ - - @@ -37,10 +35,15 @@ - + + + - + + + attributes + %kernel.cache_dir%/sessions @@ -56,6 +59,11 @@ + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml index 582f3951acc77..886132ff4b1c8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/validator.xml @@ -5,7 +5,6 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> - %kernel.cache_dir%/validation.php @@ -17,13 +16,13 @@ - + - + %validator.translation_domain% @@ -39,13 +38,14 @@ - - - - %validator.mapping.cache.file% - - - + + The "%service_id%" service is deprecated since Symfony 4.4. Use validator.mapping.cache.adapter instead. + + + + + %validator.mapping.cache.file% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml index 8cc62a72a68e5..aff90a584b87d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/web.xml @@ -88,5 +88,19 @@ + + + + %kernel.error_controller% + + + + + + + %kernel.error_controller% + + %kernel.debug% + diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php index 48f5c2c30dfd5..74b5e6f72c23e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/color_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'color']); +block($form, 'form_widget_simple', ['type' => $type ?? 'color']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php index fa6b0ee8a14a4..96bfbcacc2006 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/email_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'email']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'email']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php index b3465042efddd..a17a5ca2ab26f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/form_label.html.php @@ -1,5 +1,5 @@ - + $name, '%id%' => $id]) diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php index 6bcbb5e0461a9..15df70208e530 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/hidden_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'hidden']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'hidden']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php index faaff51e676f5..64ae6b76ec10d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/integer_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'number']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'number']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php index 3d6f79c63bedb..225aa31573664 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/number_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'text']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'text']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php index 5514468f6a1b3..ea6c6ab479c60 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/password_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'password']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'password']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php index 163e1f18f611c..47ae89f50a261 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/percent_widget.html.php @@ -1,2 +1,2 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'text']).$view->escape($symbol) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'text']).$view->escape($symbol) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php index d6650d8154676..97f4a497f5d68 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/range_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'range']); +block($form, 'form_widget_simple', ['type' => $type ?? 'range']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php index af45b1566998a..117327b16ea98 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/reset_widget.html.php @@ -1 +1 @@ -block($form, 'button_widget', ['type' => isset($type) ? $type : 'reset']) ?> +block($form, 'button_widget', ['type' => $type ?? 'reset']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php index 191279b517eb2..6b6efa7b75ed5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/search_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'search']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'search']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php index baea833326334..db643c24235e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/submit_widget.html.php @@ -1 +1 @@ -block($form, 'button_widget', ['type' => isset($type) ? $type : 'submit']) ?> +block($form, 'button_widget', ['type' => $type ?? 'submit']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php index dd471b9518d20..fb62226c1d7e2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/tel_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'tel']); +block($form, 'form_widget_simple', ['type' => $type ?? 'tel']); diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php index cd653c89c5ccc..fee20f07c979c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/url_widget.html.php @@ -1 +1 @@ -block($form, 'form_widget_simple', ['type' => isset($type) ? $type : 'url']) ?> +block($form, 'form_widget_simple', ['type' => $type ?? 'url']) ?> diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php new file mode 100644 index 0000000000000..610b6e0c19eac --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Resources/views/Form/week_widget.html.php @@ -0,0 +1,14 @@ + + block($form, 'form_widget_simple'); ?> + + ['size' => 1]] : [] ?> +
block($form, 'widget_container_attributes') ?>> + widget($form['year'], $vars); + echo '-'; + echo $view['form']->widget($form['week'], $vars); + ?> +
+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php index 51419c8914988..6e5a71b8fadfd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/AnnotatedRouteControllerLoader.php @@ -25,7 +25,7 @@ class AnnotatedRouteControllerLoader extends AnnotationClassLoader /** * Configures the _controller default parameter of a given Route instance. * - * @param mixed $annot The annotation class instance + * @param object $annot The annotation class instance */ protected function configureRoute(Route $route, \ReflectionClass $class, \ReflectionMethod $method, $annot) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php index 63e6af0be3832..7276a387d4e01 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/DelegatingLoader.php @@ -42,7 +42,7 @@ class DelegatingLoader extends BaseDelegatingLoader public function __construct($resolver, $defaultOptions = []) { if ($resolver instanceof ControllerNameParser) { - @trigger_error(sprintf('Passing a "%s" instance as first argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance instead.', ControllerNameParser::class, __METHOD__, LoaderResolverInterface::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing a "%s" instance as first argument to "%s()" is deprecated since Symfony 4.4, pass a "%s" instance instead.', ControllerNameParser::class, __METHOD__, LoaderResolverInterface::class), \E_USER_DEPRECATED); $this->parser = $resolver; $resolver = $defaultOptions; $defaultOptions = 2 < \func_num_args() ? func_get_arg(2) : []; @@ -77,7 +77,7 @@ public function load($resource, $type = null) // - this handles the case and prevents the second fatal error // by triggering an exception beforehand. - throw new LoaderLoadException($resource, null, null, null, $type); + throw new LoaderLoadException($resource, null, 0, null, $type); } $this->loading = true; @@ -95,7 +95,7 @@ public function load($resource, $type = null) continue; } - if (false !== strpos($controller, '::')) { + if (str_contains($controller, '::')) { continue; } @@ -105,17 +105,12 @@ public function load($resource, $type = null) try { $controller = $this->parser->parse($controller, false); - @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $deprecatedNotation, $controller), E_USER_DEPRECATED); + @trigger_error(sprintf('Referencing controllers with %s is deprecated since Symfony 4.1, use "%s" instead.', $deprecatedNotation, $controller), \E_USER_DEPRECATED); } catch (\InvalidArgumentException $e) { // unable to optimize unknown notation } } - if (1 === substr_count($controller, ':')) { - $nonDeprecatedNotation = str_replace(':', '::', $controller); - // TODO deprecate this in 5.1 - } - $route->setDefault('_controller', $controller); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.php b/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.php new file mode 100644 index 0000000000000..10529b459a40d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/LegacyRouteLoaderContainer.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\Bundle\FrameworkBundle\Routing; + +use Psr\Container\ContainerInterface; + +/** + * @internal to be removed in Symfony 5.0 + */ +class LegacyRouteLoaderContainer implements ContainerInterface +{ + private $container; + private $serviceLocator; + + public function __construct(ContainerInterface $container, ContainerInterface $serviceLocator) + { + $this->container = $container; + $this->serviceLocator = $serviceLocator; + } + + /** + * {@inheritdoc} + * + * @return mixed + */ + public function get($id) + { + if ($this->serviceLocator->has($id)) { + return $this->serviceLocator->get($id); + } + + @trigger_error(sprintf('Registering the service route loader "%s" without tagging it with the "routing.route_loader" tag is deprecated since Symfony 4.4 and will be required in Symfony 5.0.', $id), \E_USER_DEPRECATED); + + return $this->container->get($id); + } + + /** + * {@inheritdoc} + * + * @return bool + */ + public function has($id) + { + return $this->serviceLocator->has($id) || $this->container->has($id); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php index 2a6c6b843062a..cb2c831d969fd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableCompiledUrlMatcher.php @@ -24,7 +24,7 @@ class RedirectableCompiledUrlMatcher extends CompiledUrlMatcher implements Redir /** * {@inheritdoc} */ - public function redirect($path, $route, $scheme = null) + public function redirect($path, $route, $scheme = null): array { return [ '_controller' => 'Symfony\\Bundle\\FrameworkBundle\\Controller\\RedirectController::urlRedirectAction', diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php index 13ad12dccf866..c9ca4831845c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RedirectableUrlMatcher.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Routing; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3.', RedirectableUrlMatcher::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.3.', RedirectableUrlMatcher::class), \E_USER_DEPRECATED); use Symfony\Component\Routing\Matcher\RedirectableUrlMatcher as BaseMatcher; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/Controller/DefaultController.php b/src/Symfony/Bundle/FrameworkBundle/Routing/RouteLoaderInterface.php similarity index 62% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/Controller/DefaultController.php rename to src/Symfony/Bundle/FrameworkBundle/Routing/RouteLoaderInterface.php index c4bee6c031c89..d1cb55a7af895 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/Controller/DefaultController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/RouteLoaderInterface.php @@ -9,13 +9,11 @@ * file that was distributed with this source code. */ -namespace TestBundle\Fabpot\FooBundle\Controller; +namespace Symfony\Bundle\FrameworkBundle\Routing; /** - * DefaultController. - * - * @author Fabien Potencier + * Marker interface for service route loaders. */ -class DefaultController +interface RouteLoaderInterface { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php index c69de1fc3fbb3..50b820871427d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php +++ b/src/Symfony/Bundle/FrameworkBundle/Routing/Router.php @@ -15,15 +15,22 @@ use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\CompatibilityServiceSubscriberInterface as ServiceSubscriberInterface; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\Resource\FileExistenceResource; +use Symfony\Component\Config\Resource\FileResource; use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; use Symfony\Component\DependencyInjection\ContainerInterface as SymfonyContainerInterface; use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; use Symfony\Component\DependencyInjection\Exception\RuntimeException; use Symfony\Component\HttpKernel\CacheWarmer\WarmableInterface; +use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Router as BaseRouter; +// Help opcache.preload discover always-needed symbols +class_exists(RedirectableCompiledUrlMatcher::class); +class_exists(Route::class); + /** * This Router creates the Loader only when the cache is empty. * @@ -36,18 +43,13 @@ class Router extends BaseRouter implements WarmableInterface, ServiceSubscriberI private $paramFetcher; /** - * @param ContainerInterface $container A ContainerInterface instance - * @param mixed $resource The main resource to load - * @param array $options An array of options - * @param RequestContext $context The context - * @param ContainerInterface|null $parameters A ContainerInterface instance allowing to fetch parameters - * @param LoggerInterface|null $logger + * @param mixed $resource The main resource to load */ public function __construct(ContainerInterface $container, $resource, array $options = [], RequestContext $context = null, ContainerInterface $parameters = null, LoggerInterface $logger = null, string $defaultLocale = null) { $this->container = $container; $this->resource = $resource; - $this->context = $context ?: new RequestContext(); + $this->context = $context ?? new RequestContext(); $this->logger = $logger; $this->setOptions($options); @@ -71,6 +73,16 @@ public function getRouteCollection() $this->collection = $this->container->get('routing.loader')->load($this->resource, $this->options['resource_type']); $this->resolveParameters($this->collection); $this->collection->addResource(new ContainerParametersResource($this->collectedParameters)); + + try { + $containerFile = ($this->paramFetcher)('kernel.cache_dir').'/'.($this->paramFetcher)('kernel.container_class').'.php'; + if (file_exists($containerFile)) { + $this->collection->addResource(new FileResource($containerFile)); + } else { + $this->collection->addResource(new FileExistenceResource($containerFile)); + } + } catch (ParameterNotFoundException $exception) { + } } return $this->collection; @@ -160,23 +172,25 @@ private function resolve($value) return '%%'; } - if (preg_match('/^env\(\w+\)$/', $match[1])) { + if (preg_match('/^env\((?:\w++:)*+\w++\)$/', $match[1])) { throw new RuntimeException(sprintf('Using "%%%s%%" is not allowed in routing configuration.', $match[1])); } $resolved = ($this->paramFetcher)($match[1]); - if (\is_bool($resolved)) { - $resolved = (string) (int) $resolved; - } - - if (\is_string($resolved) || is_numeric($resolved)) { + if (\is_scalar($resolved)) { $this->collectedParameters[$match[1]] = $resolved; - return (string) $resolved; + if (\is_string($resolved)) { + $resolved = $this->resolve($resolved); + } + + if (\is_scalar($resolved)) { + return false === $resolved ? '0' : (string) $resolved; + } } - throw new RuntimeException(sprintf('The container parameter "%s", used in the route configuration value "%s", must be a string or numeric, but it is of type %s.', $match[1], $value, \gettype($resolved))); + throw new RuntimeException(sprintf('The container parameter "%s", used in the route configuration value "%s", must be a string or numeric, but it is of type "%s".', $match[1], $value, \gettype($resolved))); }, $value); return str_replace('%%', '%', $escapedValue); diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.php new file mode 100644 index 0000000000000..eeecbbb68b683 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/AbstractVault.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\Bundle\FrameworkBundle\Secrets; + +/** + * @author Nicolas Grekas + * + * @internal + */ +abstract class AbstractVault +{ + protected $lastMessage; + + public function getLastMessage(): ?string + { + return $this->lastMessage; + } + + abstract public function generateKeys(bool $override = false): bool; + + abstract public function seal(string $name, string $value): void; + + abstract public function reveal(string $name): ?string; + + abstract public function remove(string $name): bool; + + abstract public function list(bool $reveal = false): array; + + protected function validateName(string $name): void + { + if (!preg_match('/^\w++$/D', $name)) { + throw new \LogicException(sprintf('Invalid secret name "%s": only "word" characters are allowed.', $name)); + } + } + + protected function getPrettyPath(string $path) + { + return str_replace(getcwd().\DIRECTORY_SEPARATOR, '', $path); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.php new file mode 100644 index 0000000000000..3c1670feae02d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/DotenvVault.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\Bundle\FrameworkBundle\Secrets; + +/** + * @author Nicolas Grekas + * + * @internal + */ +class DotenvVault extends AbstractVault +{ + private $dotenvFile; + + public function __construct(string $dotenvFile) + { + $this->dotenvFile = strtr($dotenvFile, '/', \DIRECTORY_SEPARATOR); + } + + public function generateKeys(bool $override = false): bool + { + $this->lastMessage = 'The dotenv vault doesn\'t encrypt secrets thus doesn\'t need keys.'; + + return false; + } + + public function seal(string $name, string $value): void + { + $this->lastMessage = null; + $this->validateName($name); + $v = str_replace("'", "'\\''", $value); + + $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; + $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)/m", "$name='$v'", $content, -1, $count); + + if (!$count) { + $content .= "$name='$v'\n"; + } + + file_put_contents($this->dotenvFile, $content); + + $this->lastMessage = sprintf('Secret "%s" %s in "%s".', $name, $count ? 'added' : 'updated', $this->getPrettyPath($this->dotenvFile)); + } + + public function reveal(string $name): ?string + { + $this->lastMessage = null; + $this->validateName($name); + $v = \is_string($_SERVER[$name] ?? null) && !str_starts_with($name, 'HTTP_') ? $_SERVER[$name] : ($_ENV[$name] ?? null); + + if (null === $v) { + $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath($this->dotenvFile)); + + return null; + } + + return $v; + } + + public function remove(string $name): bool + { + $this->lastMessage = null; + $this->validateName($name); + + $content = file_exists($this->dotenvFile) ? file_get_contents($this->dotenvFile) : ''; + $content = preg_replace("/^$name=((\\\\'|'[^']++')++|.*)\n?/m", '', $content, -1, $count); + + if ($count) { + file_put_contents($this->dotenvFile, $content); + $this->lastMessage = sprintf('Secret "%s" removed from file "%s".', $name, $this->getPrettyPath($this->dotenvFile)); + + return true; + } + + $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath($this->dotenvFile)); + + return false; + } + + public function list(bool $reveal = false): array + { + $this->lastMessage = null; + $secrets = []; + + foreach ($_ENV as $k => $v) { + if (preg_match('/^\w+$/D', $k)) { + $secrets[$k] = $reveal ? $v : null; + } + } + + foreach ($_SERVER as $k => $v) { + if (\is_string($v) && preg_match('/^\w+$/D', $k)) { + $secrets[$k] = $reveal ? $v : null; + } + } + + return $secrets; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php new file mode 100644 index 0000000000000..9d8f1529b4419 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -0,0 +1,223 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Secrets; + +use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; +use Symfony\Component\VarExporter\VarExporter; + +/** + * @author Tobias Schultze + * @author JΓ©rΓ©my DerussΓ© + * @author Nicolas Grekas + * + * @internal + */ +class SodiumVault extends AbstractVault implements EnvVarLoaderInterface +{ + private $encryptionKey; + private $decryptionKey; + private $pathPrefix; + private $secretsDir; + + /** + * @param string|\Stringable|null $decryptionKey A string or a stringable object that defines the private key to use to decrypt the vault + * or null to store generated keys in the provided $secretsDir + */ + public function __construct(string $secretsDir, $decryptionKey = null) + { + if (null !== $decryptionKey && !\is_string($decryptionKey) && !(\is_object($decryptionKey) && method_exists($decryptionKey, '__toString'))) { + throw new \TypeError(sprintf('Decryption key should be a string or an object that implements the __toString() method, "%s" given.', \gettype($decryptionKey))); + } + + $this->pathPrefix = rtrim(strtr($secretsDir, '/', \DIRECTORY_SEPARATOR), \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.basename($secretsDir).'.'; + $this->decryptionKey = $decryptionKey; + $this->secretsDir = $secretsDir; + } + + public function generateKeys(bool $override = false): bool + { + $this->lastMessage = null; + + if (null === $this->encryptionKey && '' !== $this->decryptionKey = (string) $this->decryptionKey) { + $this->lastMessage = 'Cannot generate keys when a decryption key has been provided while instantiating the vault.'; + + return false; + } + + try { + $this->loadKeys(); + } catch (\RuntimeException $e) { + // ignore failures to load keys + } + + if ('' !== $this->decryptionKey && !file_exists($this->pathPrefix.'encrypt.public.php')) { + $this->export('encrypt.public', $this->encryptionKey); + } + + if (!$override && null !== $this->encryptionKey) { + $this->lastMessage = sprintf('Sodium keys already exist at "%s*.{public,private}" and won\'t be overridden.', $this->getPrettyPath($this->pathPrefix)); + + return false; + } + + $this->decryptionKey = sodium_crypto_box_keypair(); + $this->encryptionKey = sodium_crypto_box_publickey($this->decryptionKey); + + $this->export('encrypt.public', $this->encryptionKey); + $this->export('decrypt.private', $this->decryptionKey); + + $this->lastMessage = sprintf('Sodium keys have been generated at "%s*.public/private.php".', $this->getPrettyPath($this->pathPrefix)); + + return true; + } + + public function seal(string $name, string $value): void + { + $this->lastMessage = null; + $this->validateName($name); + $this->loadKeys(); + $this->export($name.'.'.substr(md5($name), 0, 6), sodium_crypto_box_seal($value, $this->encryptionKey ?? sodium_crypto_box_publickey($this->decryptionKey))); + + $list = $this->list(); + $list[$name] = null; + uksort($list, 'strnatcmp'); + file_put_contents($this->pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" encrypted in "%s"; you can commit it.', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); + } + + public function reveal(string $name): ?string + { + $this->lastMessage = null; + $this->validateName($name); + + if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { + $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); + + return null; + } + + if (!\function_exists('sodium_crypto_box_seal')) { + $this->lastMessage = sprintf('Secret "%s" cannot be revealed as the "sodium" PHP extension missing. Try running "composer require paragonie/sodium_compat" if you cannot enable the extension."', $name); + + return null; + } + + $this->loadKeys(); + + if ('' === $this->decryptionKey) { + $this->lastMessage = sprintf('Secret "%s" cannot be revealed as no decryption key was found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); + + return null; + } + + if (false === $value = sodium_crypto_box_seal_open(include $file, $this->decryptionKey)) { + $this->lastMessage = sprintf('Secret "%s" cannot be revealed as the wrong decryption key was provided for "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); + + return null; + } + + return $value; + } + + public function remove(string $name): bool + { + $this->lastMessage = null; + $this->validateName($name); + + if (!file_exists($file = $this->pathPrefix.$name.'.'.substr_replace(md5($name), '.php', -26))) { + $this->lastMessage = sprintf('Secret "%s" not found in "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\ 10000 DIRECTORY_SEPARATOR)); + + return false; + } + + $list = $this->list(); + unset($list[$name]); + file_put_contents($this->pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" removed from "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); + + return @unlink($file) || !file_exists($file); + } + + public function list(bool $reveal = false): array + { + $this->lastMessage = null; + + if (!file_exists($file = $this->pathPrefix.'list.php')) { + return []; + } + + $secrets = include $file; + + if (!$reveal) { + return $secrets; + } + + foreach ($secrets as $name => $value) { + $secrets[$name] = $this->reveal($name); + } + + return $secrets; + } + + public function loadEnvVars(): array + { + return $this->list(true); + } + + private function loadKeys(): void + { + if (!\function_exists('sodium_crypto_box_seal')) { + throw new \LogicException('The "sodium" PHP extension is required to deal with secrets. Alternatively, try running "composer require paragonie/sodium_compat" if you cannot enable the extension.".'); + } + + if (null !== $this->encryptionKey || '' !== $this->decryptionKey = (string) $this->decryptionKey) { + return; + } + + if (file_exists($this->pathPrefix.'decrypt.private.php')) { + $this->decryptionKey = (string) include $this->pathPrefix.'decrypt.private.php'; + } + + if (file_exists($this->pathPrefix.'encrypt.public.php')) { + $this->encryptionKey = (string) include $this->pathPrefix.'encrypt.public.php'; + } elseif ('' !== $this->decryptionKey) { + $this->encryptionKey = sodium_crypto_box_publickey($this->decryptionKey); + } else { + throw new \RuntimeException(sprintf('Encryption key not found in "%s".', \dirname($this->pathPrefix))); + } + } + + private function export(string $file, string $data): void + { + $name = basename($this->pathPrefix.$file); + $data = str_replace('%', '\x', rawurlencode($data)); + $data = sprintf("createSecretsDir(); + + if (false === file_put_contents($this->pathPrefix.$file.'.php', $data, \LOCK_EX)) { + $e = error_get_last(); + throw new \ErrorException($e['message'] ?? 'Failed to write secrets data.', 0, $e['type'] ?? \E_USER_WARNING); + } + } + + private function createSecretsDir(): void + { + if ($this->secretsDir && !is_dir($this->secretsDir) && !@mkdir($this->secretsDir, 0777, true) && !is_dir($this->secretsDir)) { + throw new \RuntimeException(sprintf('Unable to create the secrets directory (%s).', $this->secretsDir)); + } + + $this->secretsDir = null; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php index bd292e468160b..f1d4d8a93c7c8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/DelegatingEngine.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.DelegatingEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.DelegatingEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php b/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php index 2539980b9d960..28b023bb0348a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/EngineInterface.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.EngineInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.EngineInterface::class.' interface is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Templating\EngineInterface as BaseEngineInterface; @@ -28,9 +28,8 @@ interface EngineInterface extends BaseEngineInterface /** * Renders a view and returns a Response. * - * @param string $view The view name - * @param array $parameters An array of parameters to pass to the view - * @param Response $response A Response instance + * @param string $view The view name + * @param array $parameters An array of parameters to pass to the view * * @return Response A Response instance * diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php index 2981eb66422d1..29e45ac7f15f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/GlobalVariables.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.GlobalVariables::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.GlobalVariables::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\Request; @@ -40,7 +40,7 @@ public function __construct(ContainerInterface $container) public function getToken() { if (!$this->container->has('security.token_storage')) { - return; + return null; } return $this->container->get('security.token_storage')->getToken(); @@ -49,15 +49,12 @@ public function getToken() public function getUser() { if (!$token = $this->getToken()) { - return; + return null; } $user = $token->getUser(); - if (!\is_object($user)) { - return; - } - return $user; + return \is_object($user) ? $user : null; } /** @@ -65,9 +62,7 @@ public function getUser() */ public function getRequest() { - if ($this->container->has('request_stack')) { - return $this->container->get('request_stack')->getCurrentRequest(); - } + return $this->container->has('request_stack') ? $this->container->get('request_stack')->getCurrentRequest() : null; } /** @@ -75,9 +70,9 @@ public function getRequest() */ public function getSession() { - if ($request = $this->getRequest()) { - return $request->getSession(); - } + $request = $this->getRequest(); + + return $request && $request->hasSession() ? $request->getSession() : null; } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php index 8a054162fb81a..3e689f5934ead 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/ActionsHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.ActionsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.ActionsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\HttpKernel\Controller\ControllerReference; use Symfony\Component\HttpKernel\Fragment\FragmentHandler; @@ -36,8 +36,7 @@ public function __construct(FragmentHandler $handler) /** * Returns the fragment content for a given URI. * - * @param string $uri A URI - * @param array $options An array of options + * @param string $uri * * @return string The fragment content * @@ -45,7 +44,7 @@ public function __construct(FragmentHandler $handler) */ public function render($uri, array $options = []) { - $strategy = isset($options['strategy']) ? $options['strategy'] : 'inline'; + $strategy = $options['strategy'] ?? 'inline'; unset($options['strategy']); return $this->handler->render($uri, $strategy, $options); diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php index ff237d6a9728a..0e326b628d4f9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/AssetsHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.AssetsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.AssetsHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Asset\Packages; use Symfony\Component\Templating\Helper\Helper; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php index 1d8885ba40b54..b0a7291d93ab4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/CodeHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.CodeHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.CodeHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\Templating\Helper\Helper; @@ -36,7 +36,7 @@ class CodeHelper extends Helper */ public function __construct($fileLinkFormat, string $projectDir, string $charset) { - $this->fileLinkFormat = $fileLinkFormat ?: ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); + $this->fileLinkFormat = $fileLinkFormat ?: \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format'); $this->rootDir = str_replace('\\', '/', $projectDir).'/'; $this->charset = $charset; } @@ -63,8 +63,8 @@ public function abbrClass($class) public function abbrMethod($method) { - if (false !== strpos($method, '::')) { - list($class, $method) = explode('::', $method, 2); + if (str_contains($method, '::')) { + [$class, $method] = explode('::', $method, 2); $result = sprintf('%s::%s()', $this->abbrClass($class), $method); } elseif ('Closure' === $method) { $result = sprintf('%1$s', $method); @@ -93,7 +93,7 @@ public function formatArgs(array $args) } elseif ('array' === $item[0]) { $formattedValue = sprintf('array(%s)', \is_array($item[1]) ? $this->formatArgs($item[1]) : $item[1]); } elseif ('string' === $item[0]) { - $formattedValue = sprintf("'%s'", htmlspecialchars($item[1], ENT_QUOTES, $this->getCharset())); + $formattedValue = sprintf("'%s'", htmlspecialchars($item[1], \ENT_QUOTES, $this->getCharset())); } elseif ('null' === $item[0]) { $formattedValue = 'null'; } elseif ('boolean' === $item[0]) { @@ -101,7 +101,7 @@ public function formatArgs(array $args) } elseif ('resource' === $item[0]) { $formattedValue = 'resource'; } else { - $formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], ENT_QUOTES, $this->getCharset()), true)); + $formattedValue = str_replace("\n", '', var_export(htmlspecialchars((string) $item[1], \ENT_QUOTES, $this->getCharset()), true)); } $result[] = \is_int($key) ? $formattedValue : sprintf("'%s' => %s", $key, $formattedValue); @@ -116,7 +116,7 @@ public function formatArgs(array $args) * @param string $file A file path * @param int $line The selected line number * - * @return string An HTML string + * @return string|null An HTML string */ public function fileExcerpt($file, $line) { @@ -125,13 +125,13 @@ public function fileExcerpt($file, $line) $finfo = new \finfo(); // Check if the file is an application/octet-stream (eg. Phar file) because highlight_file cannot parse these files - if ('application/octet-stream' === $finfo->file($file, FILEINFO_MIME_TYPE)) { - return; + if ('application/octet-stream' === $finfo->file($file, \FILEINFO_MIME_TYPE)) { + return ''; } } // highlight_file could throw warnings - // see https://bugs.php.net/bug.php?id=25725 + // see https://bugs.php.net/25725 $code = @highlight_file($file, true); // remove main code/span tags $code = preg_replace('#^\s*(.*)\s*#s', '\\1', $code); @@ -144,6 +144,8 @@ public function fileExcerpt($file, $line) return '
    '.implode("\n", $lines).'
'; } + + return null; } /** @@ -157,12 +159,12 @@ public function fileExcerpt($file, $line) */ public function formatFile($file, $line, $text = null) { - $flags = ENT_QUOTES | ENT_SUBSTITUTE; + $flags = \ENT_QUOTES | \ENT_SUBSTITUTE; if (null === $text) { $file = trim($file); $fileStr = $file; - if (0 === strpos($fileStr, $this->rootDir)) { + if (str_starts_with($fileStr, $this->rootDir)) { $fileStr = str_replace(['\\', $this->rootDir], ['/', ''], $fileStr); $fileStr = htmlspecialchars($fileStr, $flags, $this->charset); $fileStr = sprintf('kernel.project_dir/%s', htmlspecialchars($this->rootDir, $flags, $this->charset), $fileStr); diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php index 0956d31adaf13..cb18d3eb34406 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/FormHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.FormHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.FormHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Form\FormRendererInterface; use Symfony\Component\Form\FormView; @@ -47,7 +47,6 @@ public function getName() * * The theme format is ":". * - * @param FormView $view A FormView instance * @param string|array $themes A theme or an array of theme * @param bool $useDefaultThemes If true, will use default themes defined in the renderer */ @@ -75,8 +74,7 @@ public function setTheme(FormView $view, $themes, $useDefaultThemes = true) * form individually. You can also create a custom form theme to adapt * the look of the form. * - * @param FormView $view The view for which to render the form - * @param array $variables Additional variables passed to the template + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -92,8 +90,7 @@ public function form(FormView $view, array $variables = []) * * start($form) ?>> * - * @param FormView $view The view for which to render the start tag - * @param array $variables Additional variables passed to the template + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -109,8 +106,7 @@ public function start(FormView $view, array $variables = []) * * end($form) ?>> * - * @param FormView $view The view for which to render the end tag - * @param array $variables Additional variables passed to the template + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -132,8 +128,7 @@ public function end(FormView $view, array $variables = []) * * widget($form, ['separator' => '+++++']) ?> * - * @param FormView $view The view for which to render the widget - * @param array $variables Additional variables passed to the template + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -145,8 +140,7 @@ public function widget(FormView $view, array $variables = []) /** * Renders the entire form field "row". * - * @param FormView $view The view for which to render the row - * @param array $variables Additional variables passed to the template + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -158,9 +152,8 @@ public function row(FormView $view, array $variables = []) /** * Renders the label of the given view. * - * @param FormView $view The view for which to render the label - * @param string $label The label - * @param array $variables Additional variables passed to the template + * @param string $label The label + * @param array $variables Additional variables passed to the template * * @return string The HTML markup */ @@ -176,8 +169,6 @@ public function label(FormView $view, $label = null, array $variables = []) /** * Renders the help of the given view. * - * @param FormView $view The parent view - * * @return string The HTML markup */ public function help(FormView $view): string @@ -198,8 +189,7 @@ public function errors(FormView $view) /** * Renders views which have not already been rendered. * - * @param FormView $view The parent view - * @param array $variables An array of variables + * @param array $variables An array of variables * * @return string The HTML markup */ @@ -211,9 +201,8 @@ public function rest(FormView $view, array $variables = []) /** * Renders a block of the template. * - * @param FormView $view The view for determining the used themes - * @param string $blockName The name of the block to render - * @param array $variables The variable to pass to the template + * @param string $blockName The name of the block to render + * @param array $variables The variable to pass to the template * * @return string The HTML markup */ @@ -259,9 +248,9 @@ public function humanize($text) public function formEncodeCurrency($text, $widget = '') { if ('UTF-8' === $charset = $this->getCharset()) { - $text = htmlspecialchars($text, ENT_QUOTES | (\defined('ENT_SUBSTITUTE') ? ENT_SUBSTITUTE : 0), 'UTF-8'); + $text = htmlspecialchars($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); } else { - $text = htmlentities($text, ENT_QUOTES | (\defined('ENT_SUBSTITUTE') ? ENT_SUBSTITUTE : 0), 'UTF-8'); + $text = htmlentities($text, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); $text = iconv('UTF-8', $charset, $text); $widget = iconv('UTF-8', $charset, $widget); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php index 351ed712c4a54..0df1f6761d0f1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RequestHelper.php @@ -11,8 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.RequestHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.RequestHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\Templating\Helper\Helper; @@ -57,7 +58,7 @@ public function getLocale() return $this->getRequest()->getLocale(); } - private function getRequest() + private function getRequest(): Request { if (!$this->requestStack->getCurrentRequest()) { throw new \LogicException('A Request must be available.'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php index 0854fe3f048bd..ad02bca83929c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/RouterHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.RouterHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.RouterHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Templating\Helper\Helper; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php index 86c0fcda1b936..33c0e6cea729a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/SessionHelper.php @@ -11,9 +11,10 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.SessionHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.SessionHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Session\SessionInterface; use Symfony\Component\Templating\Helper\Helper; /** @@ -61,7 +62,7 @@ public function hasFlash($name) return $this->getSession()->getFlashBag()->has($name); } - private function getSession() + private function getSession(): SessionInterface { if (null === $this->session) { if (!$this->requestStack->getMasterRequest()) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php index 9ec4df47a1323..57d4f1900d59e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/StopwatchHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.StopwatchHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.StopwatchHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Stopwatch\Stopwatch; use Symfony\Component\Templating\Helper\Helper; @@ -39,12 +39,14 @@ public function getName() public function __call($method, $arguments = []) { - if (null !== $this->stopwatch) { - if (method_exists($this->stopwatch, $method)) { - return $this->stopwatch->{$method}(...$arguments); - } + if (null === $this->stopwatch) { + return null; + } - throw new \BadMethodCallException(sprintf('Method "%s" of Stopwatch does not exist', $method)); + if (method_exists($this->stopwatch, $method)) { + return $this->stopwatch->{$method}(...$arguments); } + + throw new \BadMethodCallException(sprintf('Method "%s" of Stopwatch does not exist.', $method)); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php index b86c0da666aa2..e33197b832e8d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Helper/TranslatorHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Helper; -@trigger_error('The '.TranslatorHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TranslatorHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Templating\Helper\Helper; use Symfony\Component\Translation\TranslatorInterface as LegacyTranslatorInterface; @@ -39,7 +39,7 @@ class TranslatorHelper extends Helper public function __construct($translator = null) { if (null !== $translator && !$translator instanceof LegacyTranslatorInterface && !$translator instanceof TranslatorInterface) { - throw new \TypeError(sprintf('Argument 1 passed to %s() must be an instance of %s, %s given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); + throw new \TypeError(sprintf('Argument 1 passed to "%s()" must be an instance of "%s", "%s" given.', __METHOD__, TranslatorInterface::class, \is_object($translator) ? \get_class($translator) : \gettype($translator))); } $this->translator = $translator; } @@ -62,7 +62,7 @@ public function trans($id, array $parameters = [], $domain = 'messages', $locale */ public function transChoice($id, $number, array $parameters = [], $domain = 'messages', $locale = null) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), \E_USER_DEPRECATED); if (null === $this->translator) { return $this->doTrans($id, ['%count%' => $number] + $parameters, $domain, $locale); diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php index 52edeb86199fe..e060a6d7d2238 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/FilesystemLoader.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Loader; -@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Templating\Loader\LoaderInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php index 39ebe0e1d3d45..d1c6648268181 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/Loader/TemplateLocator.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating\Loader; -@trigger_error('The '.TemplateLocator::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateLocator::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Templating\TemplateReferenceInterface; @@ -31,8 +31,7 @@ class TemplateLocator implements FileLocatorInterface private $cacheHits = []; /** - * @param FileLocatorInterface $locator A FileLocatorInterface instance - * @param string $cacheDir The cache path + * @param string $cacheDir The cache path */ public function __construct(FileLocatorInterface $locator, string $cacheDir = null) { @@ -83,7 +82,7 @@ public function locate($template, $currentPath = null, $first = true) try { return $this->cacheHits[$key] = $this->locator->locate($template->getPath(), $currentPath); } catch (\InvalidArgumentException $e) { - throw new \InvalidArgumentException(sprintf('Unable to find template "%s" : "%s".', $template, $e->getMessage()), 0, $e); + throw new \InvalidArgumentException(sprintf('Unable to find template "%s": ', $template).$e->getMessage(), 0, $e); } } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php index 080d0b7815210..f1f9347b7b807 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/PhpEngine.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.PhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.PhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Psr\Container\ContainerInterface; use Symfony\Component\HttpFoundation\Response; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php index de383461d7f8a..b7c56cefb1754 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateFilenameParser.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.TemplateFilenameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateFilenameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Templating\TemplateReferenceInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php index ffa9a923f1f9d..e553b85f03ffa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateNameParser.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.TemplateNameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateNameParser::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Templating\TemplateNameParser as BaseTemplateNameParser; @@ -48,13 +48,13 @@ public function parse($name) } // normalize name - $name = str_replace(':/', ':', preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name))); + $name = preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name)); - if (false !== strpos($name, '..')) { + if (str_contains($name, '..')) { throw new \RuntimeException(sprintf('Template name "%s" contains invalid characters.', $name)); } - if (!preg_match('/^(?:([^:]*):([^:]*):)?(.+)\.([^\.]+)\.([^\.]+)$/', $name, $matches) || 0 === strpos($name, '@')) { + if (!preg_match('/^(?:([^:]*):([^:]*):)?(.+)\.([^\.]+)\.([^\.]+)$/', $name, $matches) || str_starts_with($name, '@')) { return parent::parse($name); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php index 3bc461ebb66e8..4bf4778a801b7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TemplateReference.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.TemplateReference::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateReference::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Templating\TemplateReference as BaseTemplateReference; diff --git a/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php b/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php index faa7c55fae63b..76509a8f16a7a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php +++ b/src/Symfony/Bundle/FrameworkBundle/Templating/TimedPhpEngine.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Templating; -@trigger_error('The '.TimedPhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TimedPhpEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Psr\Container\ContainerInterface; use Symfony\Component\Stopwatch\Stopwatch; diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php similarity index 83% rename from src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php rename to src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php index 8fc0997913f9c..9a0dad403063c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestCaseSetUpTearDownTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/ForwardCompatTestTrait.php @@ -19,7 +19,7 @@ /** * @internal */ - trait TestCaseSetUpTearDownTrait + trait ForwardCompatTestTrait { private function doSetUp(): void { @@ -43,13 +43,19 @@ protected function tearDown(): void /** * @internal */ - trait TestCaseSetUpTearDownTrait + trait ForwardCompatTestTrait { - private function doSetUp(): void + /** + * @return void + */ + private function doSetUp() { } - private function doTearDown(): void + /** + * @return void + */ + private function doTearDown() { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php index b27778a01a846..5df1cc87df7af 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/KernelTestCase.php @@ -23,7 +23,7 @@ */ abstract class KernelTestCase extends TestCase { - use TestCaseSetUpTearDownTrait; + use ForwardCompatTestTrait; protected static $class; @@ -37,11 +37,14 @@ abstract class KernelTestCase extends TestCase */ protected static $container; - protected static $booted; + protected static $booted = false; - protected function doTearDown(): void + private function doTearDown() { static::ensureKernelShutdown(); + static::$class = null; + static::$kernel = null; + static::$booted = false; } /** @@ -53,11 +56,11 @@ protected function doTearDown(): void protected static function getKernelClass() { if (!isset($_SERVER['KERNEL_CLASS']) && !isset($_ENV['KERNEL_CLASS'])) { - throw new \LogicException(sprintf('You must set the KERNEL_CLASS environment variable to the fully-qualified class name of your Kernel in phpunit.xml / phpunit.xml.dist or override the %1$s::createKernel() or %1$s::getKernelClass() method.', static::class)); + throw new \LogicException(sprintf('You must set the KERNEL_CLASS environment variable to the fully-qualified class name of your Kernel in phpunit.xml / phpunit.xml.dist or override the "%1$s::createKernel()" or "%1$s::getKernelClass()" method.', static::class)); } if (!class_exists($class = $_ENV['KERNEL_CLASS'] ?? $_SERVER['KERNEL_CLASS'])) { - throw new \RuntimeException(sprintf('Class "%s" doesn\'t exist or cannot be autoloaded. Check that the KERNEL_CLASS value in phpunit.xml matches the fully-qualified class name of your Kernel or override the %s::createKernel() method.', $class, static::class)); + throw new \RuntimeException(sprintf('Class "%s" doesn\'t exist or cannot be autoloaded. Check that the KERNEL_CLASS value in phpunit.xml matches the fully-qualified class name of your Kernel or override the "%s::createKernel()" method.', $class, static::class)); } return $class; @@ -72,8 +75,9 @@ protected static function bootKernel(array $options = []) { static::ensureKernelShutdown(); - static::$kernel = static::createKernel($options); - static::$kernel->boot(); + $kernel = static::createKernel($options); + $kernel->boot(); + static::$kernel = $kernel; static::$booted = true; $container = static::$kernel->getContainer(); @@ -127,13 +131,16 @@ protected static function createKernel(array $options = []) protected static function ensureKernelShutdown() { if (null !== static::$kernel) { + static::$kernel->boot(); $container = static::$kernel->getContainer(); static::$kernel->shutdown(); static::$booted = false; + if ($container instanceof ResetInterface) { $container->reset(); } } + static::$container = null; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.php new file mode 100644 index 0000000000000..fe20731a78a7b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Test/MailerAssertionsTrait.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\Bundle\FrameworkBundle\Test; + +use PHPUnit\Framework\Constraint\LogicalNot; +use Symfony\Component\Mailer\Event\MessageEvent; +use Symfony\Component\Mailer\Event\MessageEvents; +use Symfony\Component\Mailer\Test\Constraint as MailerConstraint; +use Symfony\Component\Mime\RawMessage; +use Symfony\Component\Mime\Test\Constraint as MimeConstraint; + +trait MailerAssertionsTrait +{ + public static function assertEmailCount(int $count, string $transport = null, string $message = ''): void + { + self::assertThat(self::getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport), $message); + } + + public static function assertQueuedEmailCount(int $count, string $transport = null, string $message = ''): void + { + self::assertThat(self::getMessageMailerEvents(), new MailerConstraint\EmailCount($count, $transport, true), $message); + } + + public static function assertEmailIsQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new MailerConstraint\EmailIsQueued(), $message); + } + + public static function assertEmailIsNotQueued(MessageEvent $event, string $message = ''): void + { + self::assertThat($event, new LogicalNot(new MailerConstraint\EmailIsQueued()), $message); + } + + public static function assertEmailAttachmentCount(RawMessage $email, int $count, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailAttachmentCount($count), $message); + } + + public static function assertEmailTextBodyContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailTextBodyContains($text), $message); + } + + public static function assertEmailTextBodyNotContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailTextBodyContains($text)), $message); + } + + public static function assertEmailHtmlBodyContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHtmlBodyContains($text), $message); + } + + public static function assertEmailHtmlBodyNotContains(RawMessage $email, string $text, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHtmlBodyContains($text)), $message); + } + + public static function assertEmailHasHeader(RawMessage $email, string $headerName, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHasHeader($headerName), $message); + } + + public static function assertEmailNotHasHeader(RawMessage $email, string $headerName, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHasHeader($headerName)), $message); + } + + public static function assertEmailHeaderSame(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailHeaderSame($headerName, $expectedValue), $message); + } + + public static function assertEmailHeaderNotSame(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new LogicalNot(new MimeConstraint\EmailHeaderSame($headerName, $expectedValue)), $message); + } + + public static function assertEmailAddressContains(RawMessage $email, string $headerName, string $expectedValue, string $message = ''): void + { + self::assertThat($email, new MimeConstraint\EmailAddressContains($headerName, $expectedValue), $message); + } + + /** + * @return MessageEvent[] + */ + public static function getMailerEvents(string $transport = null): array + { + return self::getMessageMailerEvents()->getEvents($transport); + } + + public static function getMailerEvent(int $index = 0, string $transport = null): ?MessageEvent + { + return self::getMailerEvents($transport)[$index] ?? null; + } + + /** + * @return RawMessage[] + */ + public static function getMailerMessages(string $transport = null): array + { + return self::getMessageMailerEvents()->getMessages($transport); + } + + public static function getMailerMessage(int $index = 0, string $transport = null): ?RawMessage + { + return self::getMailerMessages($transport)[$index] ?? null; + } + + private static function getMessageMailerEvents(): MessageEvents + { + if (!self::$container->has('mailer.logger_message_listener')) { + static::fail('A client must have Mailer enabled to make email assertions. Did you forget to require symfony/mailer?'); + } + + return self::$container->get('mailer.logger_message_listener')->getEvents(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php index 5b9c1eb51c23f..d5aec6830317e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/TestContainer.php @@ -11,7 +11,9 @@ namespace Symfony\Bundle\FrameworkBundle\Test; +use Psr\Container\ContainerInterface; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface; use Symfony\Component\HttpKernel\KernelInterface; /** @@ -41,7 +43,7 @@ public function compile() /** * {@inheritdoc} */ - public function isCompiled() + public function isCompiled(): bool { return $this->getPublicContainer()->isCompiled(); } @@ -49,13 +51,15 @@ public function isCompiled() /** * {@inheritdoc} */ - public function getParameterBag() + public function getParameterBag(): ParameterBagInterface { return $this->getPublicContainer()->getParameterBag(); } /** * {@inheritdoc} + * + * @return array|bool|float|int|string|\UnitEnum|null */ public function getParameter($name) { @@ -65,7 +69,7 @@ public function getParameter($name) /** * {@inheritdoc} */ - public function hasParameter($name) + public function hasParameter($name): bool { return $this->getPublicContainer()->hasParameter($name); } @@ -89,13 +93,15 @@ public function set($id, $service) /** * {@inheritdoc} */ - public function has($id) + public function has($id): bool { return $this->getPublicContainer()->has($id) || $this->getPrivateContainer()->has($id); } /** * {@inheritdoc} + * + * @return object|null */ public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERENCE */ 1) { @@ -105,7 +111,7 @@ public function get($id, $invalidBehavior = /* self::EXCEPTION_ON_INVALID_REFERE /** * {@inheritdoc} */ - public function initialized($id) + public function initialized($id): bool { return $this->getPublicContainer()->initialized($id); } @@ -115,13 +121,13 @@ public function initialized($id) */ public function reset() { - $this->getPublicContainer()->reset(); + // ignore the call } /** * {@inheritdoc} */ - public function getServiceIds() + public function getServiceIds(): array { return $this->getPublicContainer()->getServiceIds(); } @@ -129,12 +135,12 @@ public function getServiceIds() /** * {@inheritdoc} */ - public function getRemovedIds() + public function getRemovedIds(): array { return $this->getPublicContainer()->getRemovedIds(); } - private function getPublicContainer() + private function getPublicContainer(): Container { if (null === $container = $this->kernel->getContainer()) { throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); @@ -143,7 +149,7 @@ private function getPublicContainer() return $container; } - private function getPrivateContainer() + private function getPrivateContainer(): ContainerInterface { return $this->getPublicContainer()->get($this->privateServicesLocatorId); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php index 197f2131bd901..0f1742ee3e2ca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestAssertionsTrait.php @@ -11,11 +11,6 @@ namespace Symfony\Bundle\FrameworkBundle\Test; -/** - * Ideas borrowed from Laravel Dusk's assertions. - * - * @see https://laravel.com/docs/5.7/dusk#available-assertions - */ trait WebTestAssertionsTrait { use BrowserKitAssertionsTrait; diff --git a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php index 5da58fa5b87d6..19da82c83d0e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php +++ b/src/Symfony/Bundle/FrameworkBundle/Test/WebTestCase.php @@ -21,11 +21,13 @@ */ abstract class WebTestCase extends KernelTestCase { + use ForwardCompatTestTrait; + use MailerAssertionsTrait; use WebTestAssertionsTrait; - protected function doTearDown(): void + private function doTearDown() { - parent::doTearDown(); + parent::tearDown(); self::getClient(null); } @@ -39,8 +41,8 @@ protected function doTearDown(): void */ protected static function createClient(array $options = [], array $server = []) { - if (true === static::$booted) { - @trigger_error(sprintf('Booting the kernel before calling %s() is deprecated and will throw in Symfony 5.0, the kernel should only be booted once.', __METHOD__), E_USER_DEPRECATED); + if (static::$booted) { + @trigger_error(sprintf('Calling "%s()" while a kernel has been booted is deprecated since Symfony 4.4 and will throw an exception in 5.0, ensure the kernel is shut down before calling the method.', __METHOD__), \E_USER_DEPRECATED); } $kernel = static::bootKernel($options); @@ -51,7 +53,7 @@ protected static function createClient(array $options = [], array $server = []) if (class_exists(KernelBrowser::class)) { throw new \LogicException('You cannot create the client used in functional tests if the "framework.test" config is not set to true.'); } - throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit"'); + throw new \LogicException('You cannot create the client used in functional tests if the BrowserKit component is not available. Try running "composer require symfony/browser-kit".'); } $client->setServerParameters($server); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php index 6ad71ab459fe6..b0a408dd381c1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/AnnotationsCacheWarmerTest.php @@ -1,12 +1,24 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\CacheWarmer; use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\PsrCachedReader; use Doctrine\Common\Annotations\Reader; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Bundle\FrameworkBundle\CacheWarmer\AnnotationsCacheWarmer; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; use Symfony\Component\Cache\DoctrineProvider; @@ -16,7 +28,7 @@ class AnnotationsCacheWarmerTest extends TestCase { private $cacheDir; - protected function setUp() + protected function setUp(): void { $this->cacheDir = sys_get_temp_dir().'/'.uniqid(); $fs = new Filesystem(); @@ -24,7 +36,7 @@ protected function setUp() parent::setUp(); } - protected function tearDown() + protected function tearDown(): void { $fs = new Filesystem(); $fs->remove($this->cacheDir); @@ -41,10 +53,16 @@ public function testAnnotationsCacheWarmerWithDebugDisabled() $this->assertFileExists($cacheFile); // Assert cache is valid - $reader = new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider(new PhpArrayAdapter($cacheFile, new NullAdapter())) - ); + $reader = class_exists(PsrCachedReader::class) + ? new PsrCachedReader( + $this->getReadOnlyReader(), + new PhpArrayAdapter($cacheFile, new NullAdapter()) + ) + : new CachedReader( + $this->getReadOnlyReader(), + new DoctrineProvider(new PhpArrayAdapter($cacheFile, new NullAdapter())) + ) + ; $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); @@ -59,12 +77,21 @@ public function testAnnotationsCacheWarmerWithDebugEnabled() $warmer = new AnnotationsCacheWarmer($reader, $cacheFile, null, true); $warmer->warmUp($this->cacheDir); $this->assertFileExists($cacheFile); + // Assert cache is valid - $reader = new CachedReader( - $this->getReadOnlyReader(), - new DoctrineProvider(new PhpArrayAdapter($cacheFile, new NullAdapter())), - true - ); + $phpArrayAdapter = new PhpArrayAdapter($cacheFile, new NullAdapter()); + $reader = class_exists(PsrCachedReader::class) + ? new PsrCachedReader( + $this->getReadOnlyReader(), + $phpArrayAdapter, + true + ) + : new CachedReader( + $this->getReadOnlyReader(), + new DoctrineProvider($phpArrayAdapter), + true + ) + ; $refClass = new \ReflectionClass($this); $reader->getClassAnnotations($refClass); $reader->getMethodAnnotations($refClass->getMethod(__FUNCTION__)); @@ -72,11 +99,88 @@ public function testAnnotationsCacheWarmerWithDebugEnabled() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|Reader + * Test that the cache warming process is not broken if a class loader + * throws an exception (on class / file not found for example). + */ + public function testClassAutoloadException() + { + $this->assertFalse(class_exists($annotatedClass = 'C\C\C', false)); + + file_put_contents($this->cacheDir.'/annotations.map', sprintf('cacheDir, __FUNCTION__)); + + spl_autoload_register($classLoader = function ($class) use ($annotatedClass) { + if ($class === $annotatedClass) { + throw new \DomainException('This exception should be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp($this->cacheDir); + + spl_autoload_unregister($classLoader); + } + + /** + * Test that the cache warming process is broken if a class loader throws an + * exception but that is unrelated to the class load. + */ + public function testClassAutoloadExceptionWithUnrelatedException() + { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + + $this->assertFalse(class_exists($annotatedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_AnnotationsCacheWarmerTest', false)); + + file_put_contents($this->cacheDir.'/annotations.map', sprintf('cacheDir, __FUNCTION__)); + + spl_autoload_register($classLoader = function ($class) use ($annotatedClass) { + if ($class === $annotatedClass) { + eval('class '.$annotatedClass.'{}'); + throw new \DomainException('This exception should not be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp($this->cacheDir); + + spl_autoload_unregister($classLoader); + } + + public function testWarmupRemoveCacheMisses() + { + $cacheFile = tempnam($this->cacheDir, __FUNCTION__); + $warmer = $this->getMockBuilder(AnnotationsCacheWarmer::class) + ->setConstructorArgs([new AnnotationReader(), $cacheFile]) + ->setMethods(['doWarmUp']) + ->getMock(); + + $warmer->method('doWarmUp')->willReturnCallback(function ($cacheDir, ArrayAdapter $arrayAdapter) { + $arrayAdapter->getItem('foo_miss'); + + $item = $arrayAdapter->getItem('bar_hit'); + $item->set('data'); + $arrayAdapter->save($item); + + $item = $arrayAdapter->getItem('baz_hit_null'); + $item->set(null); + $arrayAdapter->save($item); + + return true; + }); + + $warmer->warmUp($this->cacheDir); + $data = include $cacheFile; + + $this->assertCount(1, $data[0]); + $this->assertTrue(isset($data[0]['bar_hit'])); + } + + /** + * @return MockObject&Reader */ - private function getReadOnlyReader() + private function getReadOnlyReader(): Reader { - $readerMock = $this->getMockBuilder('Doctrine\Common\Annotations\Reader')->getMock(); + $readerMock = $this->createMock(Reader::class); $readerMock->expects($this->exactly(0))->method('getClassAnnotations'); $readerMock->expects($this->exactly(0))->method('getClassAnnotation'); $readerMock->expects($this->exactly(0))->method('getMethodAnnotations'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php index ccaa64931b5c3..18eebf21e66b0 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/SerializerCacheWarmerTest.php @@ -15,7 +15,6 @@ use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Cache\Adapter\NullAdapter; use Symfony\Component\Cache\Adapter\PhpArrayAdapter; -use Symfony\Component\Serializer\Mapping\Factory\CacheClassMetadataFactory; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; @@ -23,10 +22,6 @@ class SerializerCacheWarmerTest extends TestCase { public function testWarmUp() { - if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { - $this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.'); - } - $loaders = [ new XmlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/person.xml'), new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/author.yml'), @@ -48,10 +43,6 @@ public function testWarmUp() public function testWarmUpWithoutLoader() { - if (!class_exists(CacheClassMetadataFactory::class) || !method_exists(XmlFileLoader::class, 'getMappedClasses') || !method_exists(YamlFileLoader::class, 'getMappedClasses')) { - $this->markTestSkipped('The Serializer default cache warmer has been introduced in the Serializer Component version 3.2.'); - } - $file = sys_get_temp_dir().'/cache-serializer-without-loader.php'; @unlink($file); @@ -60,4 +51,50 @@ public function testWarmUpWithoutLoader() $this->assertFileExists($file); } + + /** + * Test that the cache warming process is not broken if a class loader + * throws an exception (on class / file not found for example). + */ + public function testClassAutoloadException() + { + $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false)); + + $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__)); + + spl_autoload_register($classLoader = function ($class) use ($mappedClass) { + if ($class === $mappedClass) { + throw new \DomainException('This exception should be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp('foo'); + + spl_autoload_unregister($classLoader); + } + + /** + * Test that the cache warming process is broken if a class loader throws an + * exception but that is unrelated to the class load. + */ + public function testClassAutoloadExceptionWithUnrelatedException() + { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + + $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_SerializerCacheWarmerTest', false)); + + $warmer = new SerializerCacheWarmer([new YamlFileLoader(__DIR__.'/../Fixtures/Serialization/Resources/does_not_exist.yaml')], tempnam(sys_get_temp_dir(), __FUNCTION__)); + + spl_autoload_register($classLoader = function ($class) use ($mappedClass) { + if ($class === $mappedClass) { + eval('class '.$mappedClass.'{}'); + throw new \DomainException('This exception should not be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp('foo'); + + spl_autoload_unregister($classLoader); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php index 52b8dfad9dfbd..b81c9294259de 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplateFinderTest.php @@ -15,6 +15,7 @@ use Symfony\Bundle\FrameworkBundle\Templating\TemplateFilenameParser; use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BaseBundle\BaseBundle; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\HttpKernel\Kernel; /** * @group legacy @@ -23,12 +24,7 @@ class TemplateFinderTest extends TestCase { public function testFindAllTemplates() { - $kernel = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Kernel') - ->disableOriginalConstructor() - ->getMock() - ; - + $kernel = $this->createMock(Kernel::class); $kernel ->expects($this->any()) ->method('getBundle') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php index ea05f965f64c4..f4853162d2f09 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/TemplatePathsCacheWarmerTest.php @@ -3 57AE 8,7 +38,7 @@ class TemplatePathsCacheWarmerTest extends TestCase private $tmpDir; - protected function setUp() + protected function setUp(): void { $this->templateFinder = $this ->getMockBuilder(TemplateFinderInterface::class) @@ -59,7 +59,7 @@ protected function setUp() $this->filesystem->mkdir($this->tmpDir); } - protected function tearDown() + protected function tearDown(): void { $this->filesystem->remove($this->tmpDir); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php index 16f4ce4c0a34f..92ef379b1b819 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/CacheWarmer/ValidatorCacheWarmerTest.php @@ -76,4 +76,54 @@ public function testWarmUpWithoutLoader() $this->assertFileExists($file); } + + /** + * Test that the cache warming process is not broken if a class loader + * throws an exception (on class / file not found for example). + */ + public function testClassAutoloadException() + { + $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false)); + + $validatorBuilder = new ValidatorBuilder(); + $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml'); + $warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__)); + + spl_autoload_register($classloader = function ($class) use ($mappedClass) { + if ($class === $mappedClass) { + throw new \DomainException('This exception should be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp('foo'); + + spl_autoload_unregister($classloader); + } + + /** + * Test that the cache warming process is broken if a class loader throws an + * exception but that is unrelated to the class load. + */ + public function testClassAutoloadExceptionWithUnrelatedException() + { + $this->expectException(\DomainException::class); + $this->expectExceptionMessage('This exception should not be caught by the warmer.'); + + $this->assertFalse(class_exists($mappedClass = 'AClassThatDoesNotExist_FWB_CacheWarmer_ValidatorCacheWarmerTest', false)); + + $validatorBuilder = new ValidatorBuilder(); + $validatorBuilder->addYamlMapping(__DIR__.'/../Fixtures/Validation/Resources/does_not_exist.yaml'); + $warmer = new ValidatorCacheWarmer($validatorBuilder, tempnam(sys_get_temp_dir(), __FUNCTION__)); + + spl_autoload_register($classLoader = function ($class) use ($mappedClass) { + if ($class === $mappedClass) { + eval('class '.$mappedClass.'{}'); + throw new \DomainException('This exception should not be caught by the warmer.'); + } + }, true, true); + + $warmer->warmUp('foo'); + + spl_autoload_unregister($classLoader); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.php new file mode 100644 index 0000000000000..8a1fcf93caabd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/AboutCommandTest.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\Bundle\FrameworkBundle\Tests\Command\AboutCommand; + +use Symfony\Bundle\FrameworkBundle\Command\AboutCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\Command\AboutCommand\Fixture\TestAppKernel; +use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Exception\IOException; +use Symfony\Component\Filesystem\Filesystem; + +class AboutCommandTest extends TestCase +{ + /** @var Filesystem */ + private $fs; + + protected function setUp(): void + { + $this->fs = new Filesystem(); + } + + public function testAboutWithReadableFiles() + { + $kernel = new TestAppKernel('test', true); + $this->fs->mkdir($kernel->getProjectDir()); + + $this->fs->dumpFile($kernel->getCacheDir().'/readable_file', 'The file content.'); + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + + $tester = $this->createCommandTester($kernel); + $ret = $tester->execute([]); + + $this->assertSame(0, $ret); + $this->assertStringContainsString('Cache directory', $tester->getDisplay()); + $this->assertStringContainsString('Log directory', $tester->getDisplay()); + + $this->fs->chmod($kernel->getCacheDir().'/readable_file', 0777); + + try { + $this->fs->remove($kernel->getProjectDir()); + } catch (IOException $e) { + } + } + + public function testAboutWithUnreadableFiles() + { + $kernel = new TestAppKernel('test', true); + $this->fs->mkdir($kernel->getProjectDir()); + + // skip test on Windows; PHP can't easily set file as unreadable on Windows + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot run on Windows.'); + } + + $this->fs->dumpFile($kernel->getCacheDir().'/unreadable_file', 'The file content.'); + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0222); + + $tester = $this->createCommandTester($kernel); + $ret = $tester->execute([]); + + $this->assertSame(0, $ret); + $this->assertStringContainsString('Cache directory', $tester->getDisplay()); + $this->assertStringContainsString('Log directory', $tester->getDisplay()); + + $this->fs->chmod($kernel->getCacheDir().'/unreadable_file', 0777); + + try { + $this->fs->remove($kernel->getProjectDir()); + } catch (IOException $e) { + } + } + + private function createCommandTester(TestAppKernel $kernel): CommandTester + { + $application = new Application($kernel); + $application->add(new AboutCommand()); + + return new CommandTester($application->find('about')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.php new file mode 100644 index 0000000000000..c15bf83cb1cf8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/AboutCommand/Fixture/TestAppKernel.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\Bundle\FrameworkBundle\Tests\Command\AboutCommand\Fixture; + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Kernel; + +class TestAppKernel extends Kernel +{ + public function registerBundles(): iterable + { + return [ + new FrameworkBundle(), + ]; + } + + public function getProjectDir(): string + { + return __DIR__.'/test'; + } + + public function registerContainerConfiguration(LoaderInterface $loader) + { + } + + protected function build(ContainerBuilder $container) + { + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php index 9e4c46d585557..27f2973d82c3d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/CacheClearCommandTest.php @@ -18,6 +18,7 @@ use Symfony\Component\Config\Resource\ResourceInterface; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -28,16 +29,19 @@ class CacheClearCommandTest extends TestCase /** @var Filesystem */ private $fs; - protected function setUp() + protected function setUp(): void { $this->fs = new Filesystem(); $this->kernel = new TestAppKernel('test', true); $this->fs->mkdir($this->kernel->getProjectDir()); } - protected function tearDown() + protected function tearDown(): void { - $this->fs->remove($this->kernel->getProjectDir()); + try { + $this->fs->remove($this->kernel->getProjectDir()); + } catch (IOException $e) { + } } public function testCacheIsFreshAfterCacheClearedWithWarmup() @@ -64,7 +68,7 @@ public function testCacheIsFreshAfterCacheClearedWithWarmup() // check that app kernel file present in meta file of container's cache $containerClass = $this->kernel->getContainer()->getParameter('kernel.container_class'); $containerRef = new \ReflectionClass($containerClass); - $containerFile = \dirname(\dirname($containerRef->getFileName())).'/'.$containerClass.'.php'; + $containerFile = \dirname($containerRef->getFileName(), 2).'/'.$containerClass.'.php'; $containerMetaFile = $containerFile.'.meta'; $kernelRef = new \ReflectionObject($this->kernel); $kernelFile = $kernelRef->getFileName(); @@ -81,6 +85,6 @@ public function testCacheIsFreshAfterCacheClearedWithWarmup() $containerRef = new \ReflectionClass(require $containerFile); $containerFile = str_replace('tes_'.\DIRECTORY_SEPARATOR, 'test'.\DIRECTORY_SEPARATOR, $containerRef->getFileName()); - $this->assertRegExp(sprintf('/\'kernel.container_class\'\s*=>\s*\'%s\'/', $containerClass), file_get_contents($containerFile), 'kernel.container_class is properly set on the dumped container'); + $this->assertMatchesRegularExpression(sprintf('/\'kernel.container_class\'\s*=>\s*\'%s\'/', $containerClass), file_get_contents($containerFile), 'kernel.container_class is properly set on the dumped container'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php index cf9ca2f7e5aab..d6a58798c1369 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CacheClearCommand/Fixture/TestAppKernel.php @@ -19,14 +19,14 @@ class TestAppKernel extends Kernel { - public function registerBundles() + public function registerBundles(): iterable { return [ new FrameworkBundle(), ]; } - public function getProjectDir() + public function getProjectDir(): string { return __DIR__.'/test'; } @@ -36,6 +36,13 @@ public function registerContainerConfiguration(LoaderInterface $loader) $loader->load(__DIR__.\DIRECTORY_SEPARATOR.'config.yml'); } + public function setAnnotatedClassCache(array $annotatedClasses) + { + $annotatedClasses = array_diff($annotatedClasses, ['Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\ExceptionController', 'Symfony\Bundle\TwigBundle\Controller\PreviewErrorController']); + + parent::setAnnotatedClassCache($annotatedClasses); + } + protected function build(ContainerBuilder $container) { $container->register('logger', NullLogger::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php index e22d8542072a9..b840a538cc670 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePoolDeleteCommandTest.php @@ -11,11 +11,13 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\MockObject\MockObject; use Psr\Cache\CacheItemPoolInterface; use Symfony\Bundle\FrameworkBundle\Command\CachePoolDeleteCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\CacheClearer\Psr6CacheClearer; use Symfony\Component\HttpKernel\KernelInterface; @@ -23,10 +25,9 @@ class CachePoolDeleteCommandTest extends TestCase { private $cachePool; - protected function setUp() + protected function setUp(): void { - $this->cachePool = $this->getMockBuilder(CacheItemPoolInterface::class) - ->getMock(); + $this->cachePool = $this->createMock(CacheItemPoolInterface::class); } public function testCommandWithValidKey() @@ -44,7 +45,7 @@ public function testCommandWithValidKey() $tester = $this->getCommandTester($this->getKernel()); $tester->execute(['pool' => 'foo', 'key' => 'bar']); - $this->assertContains('[OK] Cache item "bar" was successfully deleted.', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Cache item "bar" was successfully deleted.', $tester->getDisplay()); } public function testCommandWithInValidKey() @@ -61,7 +62,7 @@ public function testCommandWithInValidKey() $tester = $this->getCommandTester($this->getKernel()); $tester->execute(['pool' => 'foo', 'key' => 'bar']); - $this->assertContains('[NOTE] Cache item "bar" does not exist in cache pool "foo".', $tester->getDisplay()); + $this->assertStringContainsString('[NOTE] Cache item "bar" does not exist in cache pool "foo".', $tester->getDisplay()); } public function testCommandDeleteFailed() @@ -76,29 +77,20 @@ public function testCommandDeleteFailed() ->with('bar') ->willReturn(false); - if (method_exists($this, 'expectExceptionMessage')) { - $this->expectExceptionMessage('Cache item "bar" could not be deleted.'); - } else { - $this->setExpectedException('Exception', 'Cache item "bar" could not be deleted.'); - } + $this->expectExceptionMessage('Cache item "bar" could not be deleted.'); $tester = $this->getCommandTester($this->getKernel()); $tester->execute(['pool' => 'foo', 'key' => 'bar']); } /** - * @return \PHPUnit_Framework_MockObject_MockObject|KernelInterface + * @return MockObject&KernelInterface */ - private function getKernel() + private function getKernel(): KernelInterface { - $container = $this - ->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') - ->getMock(); - - $kernel = $this - ->getMockBuilder(KernelInterface::class) - ->getMock(); + $container = $this->createMock(ContainerInterface::class); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php index 693fdfa10170f..32d60124ebb5a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/CachePruneCommandTest.php @@ -11,12 +11,14 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Command; +use PHPUnit\Framework\MockObject\MockObject; use Symfony\Bundle\FrameworkBundle\Command\CachePoolPruneCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\Cache\PruneableInterface; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\DependencyInjection\Argument\RewindableGenerator; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\KernelInterface; class CachePruneCommandTest extends TestCase @@ -49,18 +51,13 @@ private function getEmptyRewindableGenerator(): RewindableGenerator } /** - * @return \PHPUnit_Framework_MockObject_MockObject|KernelInterface + * @return MockObject&KernelInterface */ - private function getKernel() + private function getKernel(): KernelInterface { - $container = $this - ->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface') - ->getMock(); - - $kernel = $this - ->getMockBuilder(KernelInterface::class) - ->getMock(); + $container = $this->createMock(ContainerInterface::class); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') @@ -75,14 +72,11 @@ private function getKernel() } /** - * @return \PHPUnit_Framework_MockObject_MockObject|PruneableInterface + * @return MockObject&PruneableInterface */ - private function getPruneableInterfaceMock() + private function getPruneableInterfaceMock(): PruneableInterface { - $pruneable = $this - ->getMockBuilder(PruneableInterface::class) - ->getMock(); - + $pruneable = $this->createMock(PruneableInterface::class); $pruneable ->expects($this->atLeastOnce()) ->method('prune'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php index 497511f62908d..f5af74b98ea5f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/RouterMatchCommandTest.php @@ -16,10 +16,12 @@ use Symfony\Bundle\FrameworkBundle\Command\RouterMatchCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RouterInterface; class RouterMatchCommandTest extends TestCase { @@ -29,7 +31,7 @@ public function testWithMatchPath() $ret = $tester->execute(['path_info' => '/foo', 'foo'], ['decorated' => false]); $this->assertEquals(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('Route Name | foo', $tester->getDisplay()); + $this->assertStringContainsString('Route Name | foo', $tester->getDisplay()); } public function testWithNotMatchPath() @@ -38,13 +40,10 @@ public function testWithNotMatchPath() $ret = $tester->execute(['path_info' => '/test', 'foo'], ['decorated' => false]); $this->assertEquals(1, $ret, 'Returns 1 in case of failure'); - $this->assertContains('None of the routes match the path "/test"', $tester->getDisplay()); + $this->assertStringContainsString('None of the routes match the path "/test"', $tester->getDisplay()); } - /** - * @return CommandTester - */ - private function createCommandTester() + private function createCommandTester(): CommandTester { $application = new Application($this->getKernel()); $application->add(new RouterMatchCommand($this->getRouter())); @@ -58,7 +57,7 @@ private function getRouter() $routeCollection = new RouteCollection(); $routeCollection->add('foo', new Route('foo')); $requestContext = new RequestContext(); - $router = $this->getMockBuilder('Symfony\Component\Routing\RouterInterface')->getMock(); + $router = $this->createMock(RouterInterface::class); $router ->expects($this->any()) ->method('getRouteCollection') @@ -73,7 +72,7 @@ private function getRouter() private function getKernel() { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); $container ->expects($this->atLeastOnce()) ->method('has') @@ -88,7 +87,7 @@ private function getKernel() ->willReturn($this->getRouter()) ; - $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getContainer') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php index e22d956b125fa..d013e1b40ba7d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationDebugCommandTest.php @@ -18,6 +18,11 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\R B41A eader\TranslationReader; +use Symfony\Component\Translation\Translator; class TranslationDebugCommandTest extends TestCase { @@ -29,7 +34,7 @@ public function testDebugMissingMessages() $tester = $this->createCommandTester(['foo' => 'foo']); $tester->execute(['locale' => 'en', 'bundle' => 'foo']); - $this->assertRegExp('/missing/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); } public function testDebugUnusedMessages() @@ -37,7 +42,7 @@ public function testDebugUnusedMessages() $tester = $this->createCommandTester([], ['foo' => 'foo']); $tester->execute(['locale' => 'en', 'bundle' => 'foo']); - $this->assertRegExp('/unused/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); } public function testDebugFallbackMessages() @@ -45,7 +50,7 @@ public function testDebugFallbackMessages() $tester = $this->createCommandTester([], ['foo' => 'foo']); $tester->execute(['locale' => 'fr', 'bundle' => 'foo']); - $this->assertRegExp('/fallback/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/fallback/', $tester->getDisplay()); } public function testNoDefinedMessages() @@ -53,7 +58,7 @@ public function testNoDefinedMessages() $tester = $this->createCommandTester(); $tester->execute(['locale' => 'fr', 'bundle' => 'test']); - $this->assertRegExp('/No defined or extracted messages for locale "fr"/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/No defined or extracted messages for locale "fr"/', $tester->getDisplay()); } public function testDebugDefaultDirectory() @@ -61,8 +66,8 @@ public function testDebugDefaultDirectory() $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar']); $tester->execute(['locale' => 'en']); - $this->assertRegExp('/missing/', $tester->getDisplay()); - $this->assertRegExp('/unused/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); } /** @@ -78,8 +83,8 @@ public function testDebugLegacyDefaultDirectory() $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar']); $tester->execute(['locale' => 'en']); - $this->assertRegExp('/missing/', $tester->getDisplay()); - $this->assertRegExp('/unused/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); } public function testDebugDefaultRootDirectory() @@ -93,15 +98,15 @@ public function testDebugDefaultRootDirectory() $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar'], null, [$this->translationDir.'/trans'], [$this->translationDir.'/views']); $tester->execute(['locale' => 'en']); - $this->assertRegExp('/missing/', $tester->getDisplay()); - $this->assertRegExp('/unused/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); } public function testDebugCustomDirectory() { $this->fs->mkdir($this->translationDir.'/customDir/translations'); $this->fs->mkdir($this->translationDir.'/customDir/templates'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once()) ->method('getBundle') ->with($this->equalTo($this->translationDir.'/customDir')) @@ -110,16 +115,14 @@ public function testDebugCustomDirectory() $tester = $this->createCommandTester(['foo' => 'foo'], ['bar' => 'bar'], $kernel); $tester->execute(['locale' => 'en', 'bundle' => $this->translationDir.'/customDir']); - $this->assertRegExp('/missing/', $tester->getDisplay()); - $this->assertRegExp('/unused/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/missing/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/unused/', $tester->getDisplay()); } - /** - * @expectedException \InvalidArgumentException - */ public function testDebugInvalidDirectory() { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $this->expectException(\InvalidArgumentException::class); + $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once()) ->method('getBundle') ->with($this->equalTo('dir')) @@ -129,7 +132,7 @@ public function testDebugInvalidDirectory() $tester->execute(['locale' => 'en', 'bundle' => 'dir']); } - protected function setUp() + protected function setUp(): void { $this->fs = new Filesystem(); $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); @@ -137,26 +140,20 @@ protected function setUp() $this->fs->mkdir($this->translationDir.'/templates'); } - protected function tearDown() + protected function tearDown(): void { $this->fs->remove($this->translationDir); } - /** - * @return CommandTester - */ - private function createCommandTester($extractedMessages = [], $loadedMessages = [], $kernel = null, array $transPaths = [], array $viewsPaths = []) + private function createCommandTester($extractedMessages = [], $loadedMessages = [], $kernel = null, array $transPaths = [], array $viewsPaths = []): CommandTester { - $translator = $this->getMockBuilder('Symfony\Component\Translation\Translator') - ->disableOriginalConstructor() - ->getMock(); - + $translator = $this->createMock(Translator::class); $translator ->expects($this->any()) ->method('getFallbackLocales') ->willReturn(['en']); - $extractor = $this->getMockBuilder('Symfony\Component\Translation\Extractor\ExtractorInterface')->getMock(); + $extractor = $this->createMock(ExtractorInterface::class); $extractor ->expects($this->any()) ->method('extract') @@ -166,7 +163,7 @@ function ($path, $catalogue) use ($extractedMessages) { } ); - $loader = $this->getMockBuilder('Symfony\Component\Translation\Reader\TranslationReader')->getMock(); + $loader = $this->createMock(TranslationReader::class); $loader ->expects($this->any()) ->method('read') @@ -187,7 +184,7 @@ function ($path, $catalogue) use ($loadedMessages) { ['test', true, $this->getBundle('test')], ]; } - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getBundle') @@ -217,7 +214,7 @@ function ($path, $catalogue) use ($loadedMessages) { private function getBundle($path) { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface')->getMock(); + $bundle = $this->createMock(BundleInterface::class); $bundle ->expects($this->any()) ->method('getPath') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php index 9aedfe37b1fed..cdb438889e554 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/TranslationUpdateCommandTest.php @@ -18,6 +18,12 @@ use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Translation\Extractor\ExtractorInterface; +use Symfony\Component\Translation\Reader\TranslationReader; +use Symfony\Component\Translation\Translator; +use Symfony\Component\Translation\Writer\TranslationWriter; class TranslationUpdateCommandTest extends TestCase { @@ -28,8 +34,39 @@ public function testDumpMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); - $this->assertRegExp('/foo/', $tester->getDisplay()); - $this->assertRegExp('/1 message was successfully extracted/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); + } + + public function testDumpSortedMessagesAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); + $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'asc']); + $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); + $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); + } + + public function testDumpReverseSortedMessagesAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); + $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'desc']); + $this->assertMatchesRegularExpression("/\*test\*foo\*bar/", preg_replace('/\s+/', '', $tester->getDisplay())); + $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); + } + + public function testDumpSortWithoutValueAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); + $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort']); + $this->assertMatchesRegularExpression("/\*bar\*foo\*test/", preg_replace('/\s+/', '', $tester->getDisplay())); + $this->assertMatchesRegularExpression('/3 messages were successfully extracted/', $tester->getDisplay()); + } + + public function testDumpWrongSortAndClean() + { + $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'test' => 'test', 'bar' => 'bar']]); + $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--sort' => 'test']); + $this->assertMatchesRegularExpression('/\[ERROR\] Wrong sort order/', $tester->getDisplay()); } public function testDumpMessagesAndCleanInRootDirectory() @@ -41,32 +78,32 @@ public function testDumpMessagesAndCleanInRootDirectory() $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']], [], null, [$this->translationDir.'/trans'], [$this->translationDir.'/views']); $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--dump-messages' => true, '--clean' => true]); - $this->assertRegExp('/foo/', $tester->getDisplay()); - $this->assertRegExp('/1 message was successfully extracted/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } public function testDumpTwoMessagesAndClean() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo', 'bar' => 'bar']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true]); - $this->assertRegExp('/foo/', $tester->getDisplay()); - $this->assertRegExp('/bar/', $tester->getDisplay()); - $this->assertRegExp('/2 messages were successfully extracted/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/foo/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/2 messages were successfully extracted/', $tester->getDisplay()); } public function testDumpMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--dump-messages' => true, '--clean' => true, '--domain' => 'mydomain']); - $this->assertRegExp('/bar/', $tester->getDisplay()); - $this->assertRegExp('/1 message was successfully extracted/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/bar/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/1 message was successfully extracted/', $tester->getDisplay()); } public function testWriteMessages() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true]); - $this->assertRegExp('/Translation files were successfully updated./', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } public function testWriteMessagesInRootDirectory() @@ -78,7 +115,7 @@ public function testWriteMessagesInRootDirectory() $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--force' => true]); - $this->assertRegExp('/Translation files were successfully updated./', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } /** @@ -95,17 +132,17 @@ public function testWriteMessagesInLegacyRootDirectory() $tester = $this->createCommandTester(['messages' => ['foo' => 'foo']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', '--force' => true]); - $this->assertRegExp('/Translation files were successfully updated./', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } public function testWriteMessagesForSpecificDomain() { $tester = $this->createCommandTester(['messages' => ['foo' => 'foo'], 'mydomain' => ['bar' => 'bar']]); $tester->execute(['command' => 'translation:update', 'locale' => 'en', 'bundle' => 'foo', '--force' => true, '--domain' => 'mydomain']); - $this->assertRegExp('/Translation files were successfully updated./', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/Translation files were successfully updated./', $tester->getDisplay()); } - protected function setUp() + protected function setUp(): void { $this->fs = new Filesystem(); $this->translationDir = sys_get_temp_dir().'/'.uniqid('sf_translation', true); @@ -113,7 +150,7 @@ protected function setUp() $this->fs->mkdir($this->translationDir.'/templates'); } - protected function tearDown() + protected function tearDown(): void { $this->fs->remove($this->translationDir); } @@ -121,18 +158,15 @@ protected function tearDown() /** * @return CommandTester */ - private function createCommandTester($extractedMessages = [], $loadedMessages = [], HttpKernel\KernelInterface $kernel = null, array $transPaths = [], array $viewsPaths = []) + private function createCommandTester($extractedMessages = [], $loadedMessages = [], KernelInterface $kernel = null, array $transPaths = [], array $viewsPaths = []) { - $translator = $this->getMockBuilder('Symfony\Component\Translation\Translator') - ->disableOriginalConstructor() - ->getMock(); - + $translator = $this->createMock(Translator::class); $translator ->expects($this->any()) ->method('getFallbackLocales') ->willReturn(['en']); - $extractor = $this->getMockBuilder('Symfony\Component\Translation\Extractor\ExtractorInterface')->getMock(); + $extractor = $this->createMock(ExtractorInterface::class); $extractor ->expects($this->any()) ->method('extract') @@ -144,7 +178,7 @@ function ($path, $catalogue) use ($extractedMessages) { } ); - $loader = $this->getMockBuilder('Symfony\Component\Translation\Reader\TranslationReader')->getMock(); + $loader = $this->createMock(TranslationReader::class); $loader ->expects($this->any()) ->method('read') @@ -154,7 +188,7 @@ function ($path, $catalogue) use ($loadedMessages) { } ); - $writer = $this->getMockBuilder('Symfony\Component\Translation\Writer\TranslationWriter')->getMock(); + $writer = $this->createMock(TranslationWriter::class); $writer ->expects($this->any()) ->method('getFormats') @@ -173,7 +207,7 @@ function ($path, $catalogue) use ($loadedMessages) { ['test', true, $this->getBundle('test')], ]; } - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getBundle') @@ -202,7 +236,7 @@ function ($path, $catalogue) use ($loadedMessages) { private function getBundle($path) { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface')->getMock(); + $bundle = $this->createMock(BundleInterface::class); $bundle ->expects($this->any()) ->method('getPath') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php index 1729351a7d595..48af23514d8d9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php @@ -35,29 +35,12 @@ public function testGetHelp() { $command = new XliffLintCommand(); $expected = <<%command.name%
command lints a XLIFF file and outputs to STDOUT -the first encountered syntax error. - -You can validates XLIFF contents passed from STDIN: - - cat filename | php %command.full_name% - -You can also validate the syntax of a file: - - php %command.full_name% filename - -Or of a whole directory: - - php %command.full_name% dirname - php %command.full_name% dirname --format=json - Or find all files in a bundle: php %command.full_name% @AcmeDemoBundle - EOF; - $this->assertEquals($expected, $command->getHelp()); + $this->assertStringContainsString($expected, $command->getHelp()); } public function testLintFilesFromBundleDirectory() @@ -69,13 +52,10 @@ public function testLintFilesFromBundleDirectory() ); $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); - $this->assertContains('[OK] All 0 XLIFF files contain valid syntax', trim($tester->getDisplay())); + $this->assertStringContainsString('[OK] All 0 XLIFF files contain valid syntax', trim($tester->getDisplay())); } - /** - * @return CommandTester - */ - private function createCommandTester($application = null) + private function createCommandTester($application = null): CommandTester { if (!$application) { $application = new BaseApplication(); @@ -93,20 +73,14 @@ private function createCommandTester($application = null) private function getKernelAwareApplicationMock() { - $kernel = $this->getMockBuilder(KernelInterface::class) - ->disableOriginalConstructor() - ->getMock(); - + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->once()) ->method('locateResource') ->with('@AppBundle/Resources') ->willReturn(sys_get_temp_dir().'/xliff-lint-test'); - $application = $this->getMockBuilder(Application::class) - ->disableOriginalConstructor() - ->getMock(); - + $application = $this->createMock(Application::class); $application ->expects($this->once()) ->method('getKernel') @@ -131,13 +105,13 @@ private function getKernelAwareApplicationMock() return $application; } - protected function setUp() + protected function setUp(): void { @mkdir(sys_get_temp_dir().'/xliff-lint-test'); $this->files = []; } - protected function tearDown() + protected function tearDown(): void { foreach ($this->files as $file) { if (file_exists($file)) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php index a71fb824d57c4..0644c45ddfbad 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/YamlLintCommandTest.php @@ -41,7 +41,7 @@ public function testLintCorrectFile() ); $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); - $this->assertContains('OK', trim($tester->getDisplay())); + $this->assertStringContainsString('OK', trim($tester->getDisplay())); } public function testLintIncorrectFile() @@ -55,14 +55,12 @@ public function testLintIncorrectFile() $tester->execute(['filename' => $filename], ['decorated' => false]); $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); - $this->assertContains('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay())); + $this->assertStringContainsString('Unable to parse at line 3 (near "bar").', trim($tester->getDisplay())); } - /** - * @expectedException \RuntimeException - */ public function testLintFileNotReadable() { + $this->expectException(\RuntimeException::class); $tester = $this->createCommandTester(); $filename = $this->createFile(''); unlink($filename); @@ -74,29 +72,12 @@ public function testGetHelp() { $command = new YamlLintCommand(); $expected = <<%command.name% command lints a YAML file and outputs to STDOUT -the first encountered syntax error. - -You can validates YAML contents passed from STDIN: - - cat filename | php %command.full_name% - -You can also validate the syntax of a file: - - php %command.full_name% filename - -Or of a whole directory: - - php %command.full_name% dirname - php %command.full_name% dirname --format=json - Or find all files in a bundle: php %command.full_name% @AcmeDemoBundle - EOF; - $this->assertEquals($expected, $command->getHelp()); + $this->assertStringContainsString($expected, $command->getHelp()); } public function testLintFilesFromBundleDirectory() @@ -108,13 +89,10 @@ public function testLintFilesFromBundleDirectory() ); $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); - $this->assertContains('[OK] All 0 YAML files contain valid syntax', trim($tester->getDisplay())); + $this->assertStringContainsString('[OK] All 0 YAML files contain valid syntax', trim($tester->getDisplay())); } - /** - * @return string Path to the new file - */ - private function createFile($content) + private function createFile($content): string { $filename = tempnam(sys_get_temp_dir().'/yml-lint-test', 'sf-'); file_put_contents($filename, $content); @@ -124,10 +102,7 @@ private function createFile($content) return $filename; } - /** - * @return CommandTester - */ - private function createCommandTester($application = null) + private function createCommandTester($application = null): CommandTester { if (!$application) { $application = new BaseApplication(); @@ -145,20 +120,14 @@ private function createCommandTester($application = null) private function getKernelAwareApplicationMock() { - $kernel = $this->getMockBuilder(KernelInterface::class) - ->disableOriginalConstructor() - ->getMock(); - + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->once()) ->method('locateResource') ->with('@AppBundle/Resources') ->willReturn(sys_get_temp_dir().'/yml-lint-test'); - $application = $this->getMockBuilder(Application::class) - ->disableOriginalConstructor() - ->getMock(); - + $application = $this->createMock(Application::class); $application ->expects($this->once()) ->method('getKernel') @@ -183,19 +152,19 @@ private function getKernelAwareApplicationMock() return $application; } - protected function setUp() + protected function setUp(): void { @mkdir(sys_get_temp_dir().'/yml-lint-test'); $this->files = []; } - protected function tearDown() + protected function tearDown(): void { foreach ($this->files as $file) { if (file_exists($file)) { - unlink($file); + @unlink($file); } } - rmdir(sys_get_temp_dir().'/yml-lint-test'); + @rmdir(sys_get_temp_dir().'/yml-lint-test'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php index c9f93b96ea47e..447d2afb04125 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/ApplicationTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console; +use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Bundle\FrameworkBundle\EventListener\SuggestMissingPackageSubscriber; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; @@ -23,14 +24,18 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Tester\ApplicationTester; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\KernelInterface; class ApplicationTest extends TestCase { public function testBundleInterfaceImplementation() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface')->getMock(); + $bundle = $this->createMock(BundleInterface::class); $kernel = $this->getKernel([$bundle], true); @@ -110,7 +115,7 @@ public function testBundleCommandCanBeFoundByAlias() */ public function testBundleCommandsHaveRightContainer() { - $command = $this->getMockForAbstractClass('Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand', ['foo'], '', true, true, true, ['setContainer']); + $command = $this->getMockForAbstractClass(ContainerAwareCommand::class, ['foo'], '', true, true, true, ['setContainer']); $command->setCode(function () {}); $command->expects($this->exactly(2))->method('setContainer'); @@ -149,7 +154,7 @@ public function testRunOnlyWarnsOnUnregistrableCommand() $container->register(ThrowingCommand::class, ThrowingCommand::class); $container->setParameter('console.command.ids', [ThrowingCommand::class => ThrowingCommand::class]); - $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->method('getBundles') ->willReturn([$this->createBundleMock( @@ -167,9 +172,9 @@ public function testRunOnlyWarnsOnUnregistrableCommand() $output = $tester->getDisplay(); $this->assertSame(0, $tester->getStatusCode()); - $this->assertContains('Some commands could not be registered:', $output); - $this->assertContains('throwing', $output); - $this->assertContains('fine', $output); + $this->assertStringContainsString('Some commands could not be registered:', $output); + $this->assertStringContainsString('throwing', $output); + $this->assertStringContainsString('fine', $output); } public function testRegistrationErrorsAreDisplayedOnCommandNotFound() @@ -177,7 +182,7 @@ public function testRegistrationErrorsAreDisplayedOnCommandNotFound() $container = new ContainerBuilder(); $container->register('event_dispatcher', EventDispatcher::class); - $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->method('getBundles') ->willReturn([$this->createBundleMock( @@ -195,8 +200,8 @@ public function testRegistrationErrorsAreDisplayedOnCommandNotFound() $output = $tester->getDisplay(); $this->assertSame(1, $tester->getStatusCode()); - $this->assertContains('Some commands could not be registered:', $output); - $this->assertContains('Command "fine" is not defined.', $output); + $this->assertStringContainsString('Some commands could not be registered:', $output); + $this->assertStringContainsString('Command "fine" is not defined.', $output); } public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() @@ -206,7 +211,7 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() $container->register(ThrowingCommand::class, ThrowingCommand::class); $container->setParameter('console.command.ids', [ThrowingCommand::class => ThrowingCommand::class]); - $kernel = $this->getMockBuilder(KernelInterface::class)->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once())->method('boot'); $kernel ->method('getBundles') @@ -224,27 +229,27 @@ public function testRunOnlyWarnsOnUnregistrableCommandAtTheEnd() $tester->run(['command' => 'list']); $this->assertSame(0, $tester->getStatusCode()); - $display = explode('Lists commands', $tester->getDisplay()); + $display = explode('List commands', $tester->getDisplay()); - $this->assertContains(trim('[WARNING] Some commands could not be registered:'), trim($display[1])); + $this->assertStringContainsString(trim('[WARNING] Some commands could not be registered:'), trim($display[1])); } public function testSuggestingPackagesWithExactMatch() { $result = $this->createEventForSuggestingPackages('server:dump', []); - $this->assertRegExp('/You may be looking for a command provided by/', $result); + $this->assertMatchesRegularExpression('/You may be looking for a command provided by/', $result); } public function testSuggestingPackagesWithPartialMatchAndNoAlternatives() { $result = $this->createEventForSuggestingPackages('server', []); - $this->assertRegExp('/You may be looking for a command provided by/', $result); + $this->assertMatchesRegularExpression('/You may be looking for a command provided by/', $result); } public function testSuggestingPackagesWithPartialMatchAndAlternatives() { $result = $this->createEventForSuggestingPackages('server', ['server:run']); - $this->assertNotRegExp('/You may be looking for a command provided by/', $result); + $this->assertDoesNotMatchRegularExpression('/You may be looking for a command provided by/', $result); } private function createEventForSuggestingPackages(string $command, array $alternatives = []): string @@ -259,10 +264,10 @@ private function createEventForSuggestingPackages(string $command, array $altern private function getKernel(array $bundles, $useDispatcher = false) { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); if ($useDispatcher) { - $dispatcher = $this->getMockBuilder('Symfony\Component\EventDispatcher\EventDispatcherInterface')->getMock(); + $dispatcher = $this->createMock(EventDispatcherInterface::class); $dispatcher ->expects($this->atLeastOnce()) ->method('dispatch') @@ -287,7 +292,7 @@ private function getKernel(array $bundles, $useDispatcher = false) ->willReturnOnConsecutiveCalls([], []) ; - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel->expects($this->once())->method('boot'); $kernel ->expects($this->any()) @@ -305,7 +310,7 @@ private function getKernel(array $bundles, $useDispatcher = false) private function createBundleMock(array $commands) { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\Bundle')->getMock(); + $bundle = $this->createMock(Bundle::class); $bundle ->expects($this->once()) ->method('registerCommands') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php index 83792e28da8fd..fec5a980926c3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Style\SymfonyStyle; @@ -25,6 +26,19 @@ abstract class AbstractDescriptorTest extends TestCase { + private $colSize; + + protected function setUp(): void + { + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS=121'); + } + + protected function tearDown(): void + { + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); + } + /** @dataProvider getDescribeRouteCollectionTestData */ public function testDescribeRouteCollection(RouteCollection $routes, $expectedDescription) { @@ -108,6 +122,10 @@ public function getDescribeContainerDefinitionWithArgumentsShownTestData() $definitionsWithArgs[str_replace('definition_', 'definition_arguments_', $key)] = $definition; } + if (\PHP_VERSION_ID >= 80100) { + $definitionsWithArgs['definition_arguments_with_enum'] = (new Definition('definition_with_enum'))->setArgument(0, FooUnitEnum::FOO); + } + return $this->getDescriptionTestData($definitionsWithArgs); } @@ -190,13 +208,27 @@ public function testDescribeCallable($callable, $expectedDescription) $this->assertDescription($expectedDescription, $callable); } - public function getDescribeCallableTestData() + public function getDescribeCallableTestData(): array { return $this->getDescriptionTestData(ObjectsProvider::getCallables()); } + /** + * @group legacy + * @dataProvider getDescribeDeprecatedCallableTestData + */ + public function testDescribeDeprecatedCallable($callable, $expectedDescription) + { + $this->assertDescription($expectedDescription, $callable); + } + + public function getDescribeDeprecatedCallableTestData(): array + { + return $this->getDescriptionTestData(ObjectsProvider::getDeprecatedCallables()); + } + /** @dataProvider getClassDescriptionTestData */ - public function testGetClassDecription($object, $expectedDescription) + public function testGetClassDescription($object, $expectedDescription) { $this->assertEquals($expectedDescription, $this->getDescriptor()->getClassDescription($object)); } @@ -229,13 +261,13 @@ private function assertDescription($expectedDescription, $describedObject, array $this->getDescriptor()->describe($output, $describedObject, $options); if ('json' === $this->getFormat()) { - $this->assertEquals(json_encode(json_decode($expectedDescription), JSON_PRETTY_PRINT), json_encode(json_decode($output->fetch()), JSON_PRETTY_PRINT)); + $this->assertEquals(json_encode(json_decode($expectedDescription), \JSON_PRETTY_PRINT), json_encode(json_decode($output->fetch()), \JSON_PRETTY_PRINT)); } else { - $this->assertEquals(trim($expectedDescription), trim(str_replace(PHP_EOL, "\n", $output->fetch()))); + $this->assertEquals(trim($expectedDescription), trim(str_replace(\PHP_EOL, "\n", $output->fetch()))); } } - private function getDescriptionTestData(array $objects) + private function getDescriptionTestData(iterable $objects) { $data = []; foreach ($objects as $name => $object) { @@ -287,4 +319,25 @@ private function getEventDispatcherDescriptionTestData(array $objects) return $data; } + + /** @dataProvider getDescribeContainerBuilderWithPriorityTagsTestData */ + public function testDescribeContainerBuilderWithPriorityTags(ContainerBuilder $builder, $expectedDescription, array $options) + { + $this->assertDescription($expectedDescription, $builder, $options); + } + + public function getDescribeContainerBuilderWithPriorityTagsTestData(): array + { + $variations = ['priority_tag' => ['tag' => 'tag1']]; + $data = []; + foreach (ObjectsProvider::getContainerBuildersWithPriorityTags() as $name => $object) { + foreach ($variations as $suffix => $options) { + $file = sprintf('%s_%s.%s', trim($name, '.'), $suffix, $this->getFormat()); + $description = file_get_contents(__DIR__.'/../../Fixtures/Descriptor/'.$file); + $data[] = [$object, $description, $options, $file]; + } + } + + return $data; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php index d5491e8e345cd..4d96e90f0ef27 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -11,6 +11,8 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Suit; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -61,14 +63,26 @@ public static function getRoutes() public static function getContainerParameters() { - return [ - 'parameters_1' => new ParameterBag([ - 'integer' => 12, - 'string' => 'Hello world!', - 'boolean' => true, - 'array' => [12, 'Hello world!', true], - ]), - ]; + yield 'parameters_1' => new ParameterBag([ + 'integer' => 12, + 'string' => 'Hello world!', + 'boolean' => true, + 'array' => [12, 'Hello world!', true], + ]); + + if (\PHP_VERSION_ID < 80100) { + return; + } + + yield 'parameters_enums' => new ParameterBag([ + 'unit_enum' => FooUnitEnum::BAR, + 'backed_enum' => Suit::Hearts, + 'array_of_enums' => Suit::cases(), + 'map' => [ + 'mixed' => [Suit::Hearts, FooUnitEnum::BAR], + 'single' => FooUnitEnum::BAR, + ], + ]); } public static function getContainerParameter() @@ -109,6 +123,7 @@ public static function getContainerDefinitions() { $definition1 = new Definition('Full\\Qualified\\Class1'); $definition2 = new Definition('Full\\Qualified\\Class2'); + $definition3 = new Definition('Full\\Qualified\\Class3'); return [ 'definition_1' => $definition1 @@ -140,10 +155,65 @@ public static function getContainerDefinitions() ->addTag('tag2') ->addMethodCall('setMailer', [new Reference('mailer')]) ->setFactory([new Reference('factory.service'), 'get']), + '.definition_3' => $definition3 + ->setFile('/path/to/file') + ->setFactory([new Definition('Full\\Qualified\\FactoryClass'), 'get']), 'definition_without_class' => new Definition(), ]; } + public static function getContainerBuildersWithPriorityTags() + { + $builder = new ContainerBuilder(); + $builder->setDefinitions(self::getContainerDefinitionsWithPriorityTags()); + + return ['builder' => $builder]; + } + + public static function getContainerDefinitionsWithPriorityTags() + { + $definition1 = new Definition('Full\\Qualified\\Class1'); + $definition2 = new Definition('Full\\Qualified\\Class2'); + $definition3 = new Definition('Full\\Qualified\\Class3'); + $definition4 = new Definition('Full\\Qualified\\Class4'); + + return [ + 'definition_1' => $definition1 + ->setPublic(true) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setAbstract(false) + ->addTag('tag1', ['attr1' => 'val1', 'priority' => 30]) + ->addTag('tag1', ['attr2' => 'val2']) + ->addTag('tag2') + ->addMethodCall('setMailer', [new Reference('mailer')]) + ->setFactory([new Reference('factory.service'), 'get']), + 'definition_2' => $definition2 + ->setPublic(true) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setAbstract(false) + ->addTag('tag1', ['attr1' => 'val1', 'attr2' => 'val2', 'priority' => -20]), + 'definition_3' => $definition3 + ->setPublic(true) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setAbstract(false) + ->addTag('tag1', ['attr1' => 'val1', 'attr2' => 'val2', 'priority' => 0]) + ->addTag('tag1', ['attr3' => 'val3', 'priority' => 40]), + 'definition_4' => $definition4 + ->setPublic(true) + ->setSynthetic(true) + ->setFile('/path/to/file') + ->setLazy(false) + ->setAbstract(false) + ->addTag('tag1', ['priority' => 0]), + ]; + } + public static function getContainerAliases() { return [ @@ -156,26 +226,32 @@ public static function getEventDispatchers() { $eventDispatcher = new EventDispatcher(); - $eventDispatcher->addListener('event1', 'global_function', 255); + $eventDispatcher->addListener('event1', 'var_dump', 255); $eventDispatcher->addListener('event1', function () { return 'Closure'; }, -1); $eventDispatcher->addListener('event2', new CallableClass()); return ['event_dispatcher_1' => $eventDispatcher]; } - public static function F438 getCallables() + public static function getCallables(): array { return [ 'callable_1' => 'array_key_exists', 'callable_2' => ['Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass', 'staticMethod'], 'callable_3' => [new CallableClass(), 'method'], 'callable_4' => 'Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\CallableClass::staticMethod', - 'callable_5' => ['Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass', 'parent::staticMethod'], 'callable_6' => function () { return 'Closure'; }, 'callable_7' => new CallableClass(), 'callable_from_callable' => \Closure::fromCallable(new CallableClass()), ]; } + + public static function getDeprecatedCallables(): array + { + return [ + 'callable_5' => ['Symfony\\Bundle\\FrameworkBundle\\Tests\\Console\\Descriptor\\ExtendedCallableClass', 'parent::staticMethod'], + ]; + } } class CallableClass @@ -202,7 +278,7 @@ public static function staticMethod() class RouteStub extends Route { - public function compile() + public function compile(): CompiledRoute { return new CompiledRoute('', '#PATH_REGEX#', [], [], '#HOST_REGEX#'); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php index e775ac7cf199a..ce4f377c508fd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php @@ -15,16 +15,6 @@ class TextDescriptorTest extends AbstractDescriptorTest { - protected function setUp() - { - putenv('COLUMNS=121'); - } - - protected function tearDown() - { - putenv('COLUMNS'); - } - protected function getDescriptor() { return new TextDescriptor(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php index 9c1e0b8d9a51d..ccaa415e15955 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/AbstractControllerTest.php @@ -14,6 +14,7 @@ use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\ParameterBag\ContainerBag; use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; @@ -39,7 +40,7 @@ public function testSubscribedServices() 'security.authorization_checker' => '?Symfony\\Component\\Security\\Core\\Authorization\\AuthorizationCheckerInterface', 'templating' => '?Symfony\\Component\\Templating\\EngineInterface', 'twig' => '?Twig\\Environment', - 'doctrine' => '?Doctrine\\Common\\Persistence\\ManagerRegistry', + 'doctrine' => '?Doctrine\\Persistence\\ManagerRegistry', 'form.factory' => '?Symfony\\Component\\Form\\FormFactoryInterface', 'parameter_bag' => '?Symfony\\Component\\DependencyInjection\\ParameterBag\\ContainerBagInterface', 'message_bus' => '?Symfony\\Component\\Messenger\\MessageBusInterface', @@ -66,12 +67,10 @@ public function testGetParameter() $this->assertSame('bar', $controller->getParameter('foo')); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - * @expectedExceptionMessage TestAbstractController::getParameter()" method is missing a parameter bag - */ public function testMissingParameterBag() { + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('TestAbstractController::getParameter()" method is missing a parameter bag'); $container = new Container(); $controller = $this->createController(); @@ -83,8 +82,6 @@ public function testMissingParameterBag() class TestAbstractController extends AbstractController { - use TestControllerTrait; - private $throwOnUnexpectedService; public function __construct($throwOnUnexpectedService = true) @@ -92,7 +89,12 @@ public function __construct($throwOnUnexpectedService = true) $this->throwOnUnexpectedService = $throwOnUnexpectedService; } - public function setContainer(ContainerInterface $container) + public function __call(string $method, array $arguments) + { + return $this->$method(...$arguments); + } + + public function setContainer(ContainerInterface $container): ?ContainerInterface { if (!$this->throwOnUnexpectedService) { return parent::setContainer($container); @@ -105,22 +107,17 @@ public function setContainer(ContainerInterface $container) continue; } if (!isset($expected[$id])) { - throw new \UnexpectedValueException(sprintf('Service "%s" is not expected, as declared by %s::getSubscribedServices()', $id, AbstractController::class)); + throw new \UnexpectedValueException(sprintf('Service "%s" is not expected, as declared by "%s::getSubscribedServices()".', $id, AbstractController::class)); } $type = substr($expected[$id], 1); if (!$container->get($id) instanceof $type) { - throw new \UnexpectedValueException(sprintf('Service "%s" is expected to be an instance of "%s", as declared by %s::getSubscribedServices()', $id, $type, AbstractController::class)); + throw new \UnexpectedValueException(sprintf('Service "%s" is expected to be an instance of "%s", as declared by "%s::getSubscribedServices()".', $id, $type, AbstractController::class)); } } return parent::setContainer($container); } - public function getParameter(string $name) - { - return parent::getParameter($name); - } - public function fooAction() { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php index 1805fa074b231..c3f0cd26a85e6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerNameParserTest.php @@ -14,7 +14,9 @@ use Composer\Autoload\ClassLoader; use Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\HttpKernel\KernelInterface; /** * @group legacy @@ -23,7 +25,7 @@ class ControllerNameParserTest extends TestCase { protected $loader; - protected function setUp() + protected function setUp(): void { $this->loader = new ClassLoader(); $this->loader->add('TestBundle', __DIR__.'/../Fixtures'); @@ -31,7 +33,7 @@ protected function setUp() $this->loader->register(); } - protected function tearDown() + protected function tearDown(): void { $this->loader->unregister(); $this->loader = null; @@ -51,7 +53,7 @@ public function testParse() $parser->parse('foo:'); $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an a:b:c string'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws an \InvalidArgumentException if the controller is not an a:b:c string'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an a:b:c string'); } } @@ -66,21 +68,21 @@ public function testBuild() $parser->build('TestBundle\FooBundle\Controller\DefaultController::index'); $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } try { $parser->build('TestBundle\FooBundle\Controller\Default::indexAction'); $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } try { $parser->build('Foo\Controller\DefaultController::indexAction'); $this->fail('->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws an \InvalidArgumentException if the controller is not an aController::cAction string'); } } @@ -95,7 +97,7 @@ public function testMissingControllers($name) $parser->parse($name); $this->fail('->parse() throws a \InvalidArgumentException if the class is found but does not exist'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws a \InvalidArgumentException if the class is found but does not exist'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws a \InvalidArgumentException if the class is found but does not exist'); } } @@ -125,13 +127,13 @@ public function testInvalidBundleName($bundleName, $suggestedBundleName) $parser->parse($bundleName); $this->fail('->parse() throws a \InvalidArgumentException if the bundle does not exist'); } catch (\Exception $e) { - $this->assertInstanceOf('\InvalidArgumentException', $e, '->parse() throws a \InvalidArgumentException if the bundle does not exist'); + $this->assertInstanceOf(\InvalidArgumentException::class, $e, '->parse() throws a \InvalidArgumentException if the bundle does not exist'); if (false === $suggestedBundleName) { // make sure we don't have a suggestion - $this->assertNotContains('Did you mean', $e->getMessage()); + $this->assertStringNotContainsString('Did you mean', $e->getMessage()); } else { - $this->assertContains(sprintf('Did you mean "%s"', $suggestedBundleName), $e->getMessage()); + $this->assertStringContainsString(sprintf('Did you mean "%s"', $suggestedBundleName), $e->getMessage()); } } } @@ -151,7 +153,7 @@ private function createParser() 'FooBundle' => $this->getBundle('TestBundle\FooBundle', 'FooBundle'), ]; - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getBundle') @@ -180,7 +182,7 @@ private function createParser() private function getBundle($namespace, $name) { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface')->getMock(); + $bundle = $this->createMock(BundleInterface::class); $bundle->expects($this->any())->method('getName')->willReturn($name); $bundle->expects($this->any())->method('getNamespace')->willReturn($namespace); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php index 3eea42c24ec45..83a84cddfeef9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerResolverTest.php @@ -32,8 +32,8 @@ public function testGetControllerOnContainerAware() $controller = $resolver->getController($request); - $this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller[0]); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller[0]->getContainer()); + $this->assertInstanceOf(ContainerAwareController::class, $controller[0]); + $this->assertInstanceOf(ContainerInterface::class, $controller[0]->getContainer()); $this->assertSame('testAction', $controller[1]); } @@ -45,8 +45,8 @@ public function testGetControllerOnContainerAwareInvokable() $controller = $resolver->getController($request); - $this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller->getContainer()); + $this->assertInstanceOf(ContainerAwareController::class, $controller); + $this->assertInstanceOf(ContainerInterface::class, $controller->getContainer()); } /** @@ -69,8 +69,8 @@ public function testGetControllerWithBundleNotation() $controller = $resolver->getController($request); - $this->assertInstanceOf('Symfony\Bundle\FrameworkBundle\Tests\Controller\ContainerAwareController', $controller[0]); - $this->assertInstanceOf('Symfony\Component\DependencyInjection\ContainerInterface', $controller[0]->getContainer()); + $this->assertInstanceOf(ContainerAwareController::class, $controller[0]); + $this->assertInstanceOf(ContainerInterface::class, $controller[0]->getContainer()); $this->assertSame('testAction', $controller[1]); } @@ -200,12 +200,12 @@ protected function createControllerResolver(LoggerInterface $logger = null, Psr1 protected function createMockParser() { - return $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Controller\ControllerNameParser')->disableOriginalConstructor()->getMock(); + return $this->createMock(ControllerNameParser::class); } protected function createMockContainer() { - return $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + return $this->createMock(ContainerInterface::class); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php index ca78fdd54d126..fbeb68d6df8da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/ControllerTraitTest.php @@ -11,24 +11,39 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; -use Fig\Link\Link; -use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; +use Doctrine\Persistence\ManagerRegistry; +use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; use Symfony\Component\Form\Form; +use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\FormConfigInterface; +use Symfony\Component\Form\FormFactoryInterface; use Symfony\Component\HttpFoundation\BinaryFileResponse; +use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException; use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\HttpFoundation\Session\Flash\FlashBag; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; +use Symfony\Component\Security\Core\Exception\AccessDeniedException; use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Csrf\CsrfTokenManagerInterface; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\WebLink\Link; +use Twig\Environment; abstract class ControllerTraitTest extends TestCase { @@ -43,7 +58,7 @@ public function testForward() $requestStack = new RequestStack(); $requestStack->push($request); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel->expects($this->once())->method('handle')->willReturnCallback(function (Request $request) { return new Response($request->getRequestFormat().'--'.$request->getLocale()); }); @@ -88,26 +103,19 @@ public function testGetUserWithEmptyTokenStorage() $this->assertNull($controller->getUser()); } - /** - * @expectedException \LogicException - * @expectedExceptionMessage The SecurityBundle is not registered in your application. - */ public function testGetUserWithEmptyContainer() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The SecurityBundle is not registered in your application.'); $controller = $this->createController(); $controller->setContainer(new Container()); $controller->getUser(); } - /** - * @param $token - * - * @return Container - */ - private function getContainerWithTokenStorage($token = null) + private function getContainerWithTokenStorage($token = null): Container { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage')->getMock(); + $tokenStorage = $this->createMock(TokenStorage::class); $tokenStorage ->expects($this->once()) ->method('getToken') @@ -133,7 +141,7 @@ public function testJsonWithSerializer() { $container = new Container(); - $serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); + $serializer = $this->createMock(SerializerInterface::class); $serializer ->expects($this->once()) ->method('serialize') @@ -154,7 +162,7 @@ public function testJsonWithSerializerContextOverride() { $container = new Container(); - $serializer = $this->getMockBuilder(SerializerInterface::class)->getMock(); + $serializer = $this->createMock(SerializerInterface::class); $serializer ->expects($this->once()) ->method('serialize') @@ -169,14 +177,14 @@ public function testJsonWithSerializerContextOverride() $response = $controller->json([], 200, [], ['json_encode_options' => 0, 'other' => 'context']); $this->assertInstanceOf(JsonResponse::class, $response); $this->assertEquals('[]', $response->getContent()); - $response->setEncodingOptions(JSON_FORCE_OBJECT); + $response->setEncodingOptions(\JSON_FORCE_OBJECT); $this->assertEquals('{}', $response->getContent()); } public function testFile() { $container = new Container(); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $container->set('http_kernel', $kernel); $controller = $this->createController(); @@ -189,8 +197,8 @@ public function testFile() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertContains(basename(__FILE__), $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); } public function testFileAsInline() @@ -205,8 +213,8 @@ public function testFileAsInline() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); - $this->assertContains(basename(__FILE__), $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); } public function testFileWithOwnFileName() @@ -222,8 +230,8 @@ public function testFileWithOwnFileName() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertContains($fileName, $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); } public function testFileWithOwnFileNameAsInline() @@ -239,8 +247,8 @@ public function testFileWithOwnFileNameAsInline() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); - $this->assertContains($fileName, $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_INLINE, $response->headers->get('content-disposition')); + $this->assertStringContainsString($fileName, $response->headers->get('content-disposition')); } public function testFileFromPath() @@ -255,8 +263,8 @@ public function testFileFromPath() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertContains(basename(__FILE__), $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString(basename(__FILE__), $response->headers->get('content-disposition')); } public function testFileFromPathWithCustomizedFileName() @@ -271,24 +279,21 @@ public function testFileFromPathWithCustomizedFileName() if ($response->headers->get('content-type')) { $this->assertSame('text/x-php', $response->headers->get('content-type')); } - $this->assertContains(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); - $this->assertContains('test.php', $response->headers->get('content-disposition')); + $this->assertStringContainsString(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $response->headers->get('content-disposition')); + $this->assertStringContainsString('test.php', $response->headers->get('content-disposition')); } - /** - * @expectedException \Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException - */ public function testFileWhichDoesNotExist() { + $this->expectException(FileNotFoundException::class); $controller = $this->createController(); - /* @var BinaryFileResponse $response */ - $response = $controller->file('some-file.txt', 'test.php'); + $controller->file('some-file.txt', 'test.php'); } public function testIsGranted() { - $authorizationChecker = $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface')->getMock(); + $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(true); $container = new Container(); @@ -300,12 +305,10 @@ public function testIsGranted() $this->assertTrue($controller->isGranted('foo')); } - /** - * @expectedException \Symfony\Component\Security\Core\Exception\AccessDeniedException - */ public function testdenyAccessUnlessGranted() { - $authorizationChecker = $this->getMockBuilder('Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface')->getMock(); + $this->expectException(AccessDeniedException::class); + $authorizationChecker = $this->createMock(AuthorizationCheckerInterface::class); $authorizationChecker->expects($this->once())->method('isGranted')->willReturn(false); $container = new Container(); @@ -319,7 +322,7 @@ public function testdenyAccessUnlessGranted() public function testRenderViewTwig() { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + $twig = $this->createMock(Environment::class); $twig->expects($this->once())->method('render')->willReturn('bar'); $container = new Container(); @@ -333,7 +336,7 @@ public function testRenderViewTwig() public function testRenderTwig() { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + $twig = $this->createMock(Environment::class); $twig->expects($this->once())->method('render')->willReturn('bar'); $container = new Container(); @@ -347,7 +350,7 @@ public function testRenderTwig() public function testStreamTwig() { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + $twig = $this->createMock(Environment::class); $container = new Container(); $container->set('twig', $twig); @@ -355,12 +358,12 @@ public function testStreamTwig() $controller = $this->createController(); $controller->setContainer($container); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $controller->stream('foo')); + $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); } public function testRedirectToRoute() { - $router = $this->getMockBuilder('Symfony\Component\Routing\RouterInterface')->getMock(); + $router = $this->createMock(RouterInterface::class); $router->expects($this->once())->method('generate')->willReturn('/foo'); $container = new Container(); @@ -370,7 +373,7 @@ public function testRedirectToRoute() $controller->setContainer($container); $response = $controller->redirectToRoute('foo'); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); + $this->assertInstanceOf(RedirectResponse::class, $response); $this->assertSame('/foo', $response->getTargetUrl()); $this->assertSame(302, $response->getStatusCode()); } @@ -381,7 +384,7 @@ public function testRedirectToRoute() public function testAddFlash() { $flashBag = new FlashBag(); - $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')->getMock(); + $session = $this->createMock(Session::class); $session->expects($this->once())->method('getFlashBag')->willReturn($flashBag); $container = new Container(); @@ -398,12 +401,12 @@ public function testCreateAccessDeniedException() { $controller = $this->createController(); - $this->assertInstanceOf('Symfony\Component\Security\Core\Exception\AccessDeniedException', $controller->createAccessDeniedException()); + $this->assertInstanceOf(AccessDeniedException::class, $controller->createAccessDeniedException()); } public function testIsCsrfTokenValid() { - $tokenManager = $this->getMockBuilder('Symfony\Component\Security\Csrf\CsrfTokenManagerInterface')->getMock(); + $tokenManager = $this->createMock(CsrfTokenManagerInterface::class); $tokenManager->expects($this->once())->method('isTokenValid')->willReturn(true); $container = new Container(); @@ -417,7 +420,7 @@ public function testIsCsrfTokenValid() public function testGenerateUrl() { - $router = $this->getMockBuilder('Symfony\Component\Routing\RouterInterface')->getMock(); + $router = $this->createMock(RouterInterface::class); $router->expects($this->once())->method('generate')->willReturn('/foo'); $container = new Container(); @@ -432,10 +435,10 @@ public function testGenerateUrl() public function testRedirect() { $controller = $this->createController(); - $response = $controller->redirect('http://dunglas.fr', 301); + $response = $controller->redirect('https://dunglas.fr', 301); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\RedirectResponse', $response); - $this->assertSame('http://dunglas.fr', $response->getTargetUrl()); + $this->assertInstanceOf(RedirectResponse::class, $response); + $this->assertSame('https://dunglas.fr', $response->getTargetUrl()); $this->assertSame(301, $response->getStatusCode()); } @@ -444,8 +447,9 @@ public function testRedirect() */ public function testRenderViewTemplating() { - $templating = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')->getMock(); + $templating = $this->createMock(EngineInterface::class); $templating->expects($this->once())->method('render')->willReturn('bar'); + $templating->expects($this->once())->method('supports')->willReturn(true); $container = new Container(); $container->set('templating', $templating); @@ -461,8 +465,9 @@ public function testRenderViewTemplating() */ public function testRenderTemplating() { - $templating = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')->getMock(); + $templating = $this->createMock(EngineInterface::class); $templating->expects($this->once())->method('render')->willReturn('bar'); + $templating->expects($this->once())->method('supports')->willReturn(true); $container = new Container(); $container->set('templating', $templating); @@ -478,7 +483,7 @@ public function testRenderTemplating() */ public function testStreamTemplating() { - $templating = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')->getMock(); + $templating = $this->createMock(EngineInterface::class); $container = new Container(); $container->set('templating', $templating); @@ -486,21 +491,25 @@ public function testStreamTemplating() $controller = $this->createController(); $controller->setContainer($container); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\StreamedResponse', $controller->stream('foo')); + $this->assertInstanceOf(StreamedResponse::class, $controller->stream('foo')); } public function testCreateNotFoundException() { $controller = $this->createController(); - $this->assertInstanceOf('Symfony\Component\HttpKernel\Exception\NotFoundHttpException', $controller->createNotFoundException()); + $this->assertInstanceOf(NotFoundHttpException::class, $controller->createNotFoundException()); } public function testCreateForm() { - $form = new Form($this->getMockBuilder(FormConfigInterface::class)->getMock()); + $config = $this->createMock(FormConfigInterface::class); + $config->method('getInheritData')->willReturn(false); + $config->method('getName')->willReturn(''); - $formFactory = $this->getMockBuilder('Symfony\Component\Form\FormFactoryInterface')->getMock(); + $form = new Form($config); + + $formFactory = $this->createMock(FormFactoryInterface::class); $formFactory->expects($this->once())->method('create')->willReturn($form); $container = new Container(); @@ -514,9 +523,9 @@ public function testCreateForm() public function testCreateFormBuilder() { - $formBuilder = $this->getMockBuilder('Symfony\Component\Form\FormBuilderInterface')->getMock(); + $formBuilder = $this->createMock(FormBuilderInterface::class); - $formFactory = $this->getMockBuilder('Symfony\Component\Form\FormFactoryInterface')->getMock(); + $formFactory = $this->createMock(FormFactoryInterface::class); $formFactory->expects($this->once())->method('createBuilder')->willReturn($formBuilder); $container = new Container(); @@ -530,7 +539,7 @@ public function testCreateFormBuilder() public function testGetDoctrine() { - $doctrine = $this->getMockBuilder('Doctrine\Common\Persistence\ManagerRegistry')->getMock(); + $doctrine = $this->createMock(ManagerRegistry::class); $container = new Container(); $container->set('doctrine', $doctrine); @@ -556,29 +565,3 @@ public function testAddLink() $this->assertContains($link2, $links); } } - -trait TestControllerTrait -{ - use ControllerTrait { - generateUrl as public; - redirect as public; - forward as public; - getUser as public; - json as public; - file as public; - isGranted as public; - denyAccessUnlessGranted as public; - redirectToRoute as public; - addFlash as public; - isCsrfTokenValid as public; - renderView as public; - render as public; - stream as public; - createNotFoundException as public; - createAccessDeniedException as public; - createForm as public; - createFormBuilder as public; - getDoctrine as public; - addLink as public; - } -} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php index 1c76d0366c626..70ccf7c97cf5e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/RedirectControllerTest.php @@ -42,6 +42,22 @@ public function testEmptyRoute() } catch (HttpException $e) { $this->assertSame(404, $e->getStatusCode()); } + + $request = new Request([], [], ['_route_params' => ['route' => '', 'permanent' => true]]); + try { + $controller($request); + $this->fail('Expected Symfony\Component\HttpKernel\Exception\HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(410, $e->getStatusCode()); + } + + $request = new Request([], [], ['_route_params' => ['route' => '', 'permanent' => false]]); + try { + $controller($request); + $this->fail('Expected Symfony\Component\HttpKernel\Exception\HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(404, $e->getStatusCode()); + } } /** @@ -69,9 +85,9 @@ public function testRoute($permanent, $keepRequestMethod, $keepQueryParams, $ign $request->attributes = new ParameterBag($attributes); - $router = $this->getMockBuilder(UrlGeneratorInterface::class)->getMock(); + $router = $this->createMock(UrlGeneratorInterface::class); $router - ->expects($this->once()) + ->expects($this->exactly(2)) ->method('generate') ->with($this->equalTo($route), $this->equalTo($expectedAttributes)) ->willReturn($url); @@ -79,7 +95,10 @@ public function testRoute($permanent, $keepRequestMethod, $keepQueryParams, $ign $controller = new RedirectController($router); $returnResponse = $controller->redirectAction($request, $route, $permanent, $ignoreAttributes, $keepRequestMethod, $keepQueryParams); + $this->assertRedirectUrl($returnResponse, $url); + $this->assertEquals($expectedCode, $returnResponse->getStatusCode()); + $returnResponse = $controller($request); $this->assertRedirectUrl($returnResponse, $url); $this->assertEquals($expectedCode, $returnResponse->getStatusCode()); } @@ -116,14 +135,35 @@ public function testEmptyPath() } catch (HttpException $e) { $this->assertSame(404, $e->getStatusCode()); } + + $request = new Request([], [], ['_route_params' => ['path' => '', 'permanent' => true]]); + try { + $controller($request); + $this->fail('Expected Symfony\Component\HttpKernel\Exception\HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(410, $e->getStatusCode()); + } + + $request = new Request([], [], ['_route_params' => ['path' => '', 'permanent' => false]]); + try { + $controller($request); + $this->fail('Expected Symfony\Component\HttpKernel\Exception\HttpException to be thrown'); + } catch (HttpException $e) { + $this->assertSame(404, $e->getStatusCode()); + } } public function testFullURL() { $request = new Request(); $controller = new RedirectController(); + $returnResponse = $controller->urlRedirectAction($request, 'http://foo.bar/'); + $this->assertRedirectUrl($returnResponse, 'http://foo.bar/'); + $this->assertEquals(302, $returnResponse->getStatusCode()); + $request = new Request([], [], ['_route_params' => ['path' => 'http://foo.bar/']]); + $returnResponse = $controller($request); $this->assertRedirectUrl($returnResponse, 'http://foo.bar/'); $this->assertEquals(302, $returnResponse->getStatusCode()); } @@ -132,8 +172,13 @@ public function testFullURLWithMethodKeep() { $request = new Request(); $controller = new RedirectController(); + $returnResponse = $controller->urlRedirectAction($request, 'http://foo.bar/', false, null, null, null, true); + $this->assertRedirectUrl($returnResponse, 'http://foo.bar/'); + $this->assertEquals(307, $returnResponse->getStatusCode()); + $request = new Request([], [], ['_route_params' => ['path' => 'http://foo.bar/', 'keepRequestMethod' => true]]); + $returnResponse = $controller($request); $this->assertRedirectUrl($returnResponse, 'http://foo.bar/'); $this->assertEquals(307, $returnResponse->getStatusCode()); } @@ -151,12 +196,18 @@ public function testUrlRedirectDefaultPorts() $controller = $this->createRedirectController(null, $httpsPort); $returnValue = $controller->urlRedirectAction($request, $path, false, 'https'); $this->assertRedirectUrl($returnValue, $expectedUrl); + $request->attributes = new ParameterBag(['_route_params' => ['path' => $path, 'scheme' => 'https']]); + $returnValue = $controller($request); + $this->assertRedirectUrl($returnValue, $expectedUrl); $expectedUrl = "http://$host:$httpPort$baseUrl$path"; $request = $this->createRequestObject('https', $host, $httpPort, $baseUrl); $controller = $this->createRedirectController($httpPort); $returnValue = $controller->urlRedirectAction($request, $path, false, 'http'); $this->assertRedirectUrl($returnValue, $expectedUrl); + $request->attributes = new ParameterBag(['_route_params' => ['path' => $path, 'scheme' => 'http']]); + $returnValue = $controller($request); + $this->assertRedirectUrl($returnValue, $expectedUrl); } public function urlRedirectProvider() @@ -205,6 +256,10 @@ public function testUrlRedirect($scheme, $httpPort, $httpsPort, $requestScheme, $returnValue = $controller->urlRedirectAction($request, $path, false, $scheme, $httpPort, $httpsPort); $this->assertRedirectUrl($returnValue, $expectedUrl); + + $request->attributes = new ParameterBag(['_route_params' => ['path' => $path, 'scheme' => $scheme, 'httpPort' => $httpPort, 'httpsPort' => $httpsPort]]); + $returnValue = $controller($request); + $this->assertRedirectUrl($returnValue, $expectedUrl); } public function pathQueryParamsProvider() @@ -212,9 +267,9 @@ public function pathQueryParamsProvider() return [ ['http://www.example.com/base/redirect-path', '/redirect-path', ''], ['http://www.example.com/base/redirect-path?foo=bar', '/redirect-path?foo=bar', ''], - ['http://www.example.com/base/redirect-path?foo=bar', '/redirect-path', 'foo=bar'], - ['http://www.example.com/base/redirect-path?foo=bar&abc=example', '/redirect-path?foo=bar', 'abc=example'], - ['http://www.example.com/base/redirect-path?foo=bar&abc=example&baz=def', '/redirect-path?foo=bar', 'abc=example&baz=def'], + ['http://www.example.com/base/redirect-path?f.o=bar', '/redirect-path', 'f.o=bar'], + ['http://www.example.com/base/redirect-path?f.o=bar&a.c=example', '/redirect-path?f.o=bar', 'a.c=example'], + ['http://www.example.com/base/redirect-path?f.o=bar&a.c=example&b.z=def', '/redirect-path?f.o=bar', 'a.c=example&b.z=def'], ]; } @@ -234,6 +289,10 @@ public function testPathQueryParams($expectedUrl, $path, $queryString) $returnValue = $controller->urlRedirectAction($request, $path, false, $scheme, $port, null); $this->assertRedirectUrl($returnValue, $expectedUrl); + + $request->attributes = new ParameterBag(['_route_params' => ['path' => $path, 'scheme' => $scheme, 'httpPort' => $port]]); + $returnValue = $controller($request); + $this->assertRedirectUrl($returnValue, $expectedUrl); } public function testRedirectWithQuery() @@ -243,14 +302,19 @@ public function testRedirectWithQuery() $baseUrl = '/base'; $port = 80; - $request = $this->createRequestObject($scheme, $host, $port, $baseUrl, 'base=zaza'); - $request->query = new ParameterBag(['base' => 'zaza']); + $request = $this->createRequestObject($scheme, $host, $port, $baseUrl, 'b.se=zaza&f[%2525][%26][%3D][p.c]=d'); $request->attributes = new ParameterBag(['_route_params' => ['base2' => 'zaza']]); - $urlGenerator = $this->getMockBuilder(UrlGeneratorInterface::class)->getMock(); - $urlGenerator->expects($this->once())->method('generate')->willReturn('/test?base=zaza&base2=zaza')->with('/test', ['base' => 'zaza', 'base2' => 'zaza'], UrlGeneratorInterface::ABSOLUTE_URL); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->expects($this->exactly(2)) + ->method('generate') + ->willReturn('/test?b.se=zaza&base2=zaza&f[%2525][%26][%3D][p.c]=d') + ->with('/test', ['b.se' => 'zaza', 'base2' => 'zaza', 'f' => ['%25' => ['&' => ['=' => ['p.c' => 'd']]]]], UrlGeneratorInterface::ABSOLUTE_URL); $controller = new RedirectController($urlGenerator); - $this->assertRedirectUrl($controller->redirectAction($request, '/test', false, false, false, true), '/test?base=zaza&base2=zaza'); + $this->assertRedirectUrl($controller->redirectAction($request, '/test', false, false, false, true), '/test?b.se=zaza&base2=zaza&f[%2525][%26][%3D][p.c]=d'); + + $request->attributes->set('_route_params', ['base2' => 'zaza', 'route' => '/test', 'ignoreAttributes' => false, 'keepRequestMethod' => false, 'keepQueryParams' => true]); + $this->assertRedirectUrl($controller($request), '/test?b.se=zaza&base2=zaza&f[%2525][%26][%3D][p.c]=d'); } public function testRedirectWithQueryWithRouteParamsOveriding() @@ -260,41 +324,50 @@ public function testRedirectWithQueryWithRouteParamsOveriding() $baseUrl = '/base'; $port = 80; - $request = $this->createRequestObject($scheme, $host, $port, $baseUrl, 'base=zaza'); - $request->query = new ParameterBag(['base' => 'zaza']); - $request->attributes = new ParameterBag(['_route_params' => ['base' => 'zouzou']]); - $urlGenerator = $this->getMockBuilder(UrlGeneratorInterface::class)->getMock(); - $urlGenerator->expects($this->once())->method('generate')->willReturn('/test?base=zouzou')->with('/test', ['base' => 'zouzou'], UrlGeneratorInterface::ABSOLUTE_URL); + $request = $this->createRequestObject($scheme, $host, $port, $baseUrl, 'b.se=zaza'); + $request->attributes = new ParameterBag(['_route_params' => ['b.se' => 'zouzou']]); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $urlGenerator->expects($this->exactly(2))->method('generate')->willReturn('/test?b.se=zouzou')->with('/test', ['b.se' => 'zouzou'], UrlGeneratorInterface::ABSOLUTE_URL); $controller = new RedirectController($urlGenerator); - $this->assertRedirectUrl($controller->redirectAction($request, '/test', false, false, false, true), '/test?base=zouzou'); + $this->assertRedirectUrl($controller->redirectAction($request, '/test', false, false, false, true), '/test?b.se=zouzou'); + + $request->attributes->set('_route_params', ['b.se' => 'zouzou', 'route' => '/test', 'ignoreAttributes' => false, 'keepRequestMethod' => false, 'keepQueryParams' => true]); + $this->assertRedirectUrl($controller($request), '/test?b.se=zouzou'); + } + + public function testMissingPathOrRouteParameter() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('The parameter "path" or "route" is required to configure the redirect action in "_redirect" routing configuration.'); + + (new RedirectController())(new Request([], [], ['_route' => '_redirect'])); + } + + public function testAmbiguousPathAndRouteParameter() + { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Ambiguous redirection settings, use the "path" or "route" parameter, not both: "/foo" and "bar" found respectively in "_redirect" routing configuration.'); + + (new RedirectController())(new Request([], [], ['_route' => '_redirect', '_route_params' => ['path' => '/foo', 'route' => 'bar']])); } private function createRequestObject($scheme, $host, $port, $baseUrl, $queryString = '') { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); - $request - ->expects($this->any()) - ->method('getScheme') - ->willReturn($scheme); - $request - ->expects($this->any()) - ->method('getHost') - ->willReturn($host); - $request - ->expects($this->any()) - ->method('getPort') - ->willReturn($port); - $request - ->expects($this->any()) - ->method('getBaseUrl') - ->willReturn($baseUrl); - $request - ->expects($this->any()) - ->method('getQueryString') - ->willReturn($queryString); - - return $request; + if ('' !== $queryString) { + parse_str($queryString, $query); + } else { + $query = []; + } + + return new Request($query, [], [], [], [], [ + 'HTTPS' => 'https' === $scheme, + 'HTTP_HOST' => $host.($port ? ':'.$port : ''), + 'SERVER_PORT' => $port, + 'SCRIPT_FILENAME' => $baseUrl, + 'REQUEST_URI' => $baseUrl, + 'QUERY_STRING' => $queryString, + ]); } private function createRedirectController($httpPort = null, $httpsPort = null) diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php index a19d0651bb126..4230797b07d36 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TemplateControllerTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Controller\TemplateController; use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Twig\Environment; /** * @author KΓ©vin Dunglas @@ -22,7 +23,7 @@ class TemplateControllerTest extends TestCase { public function testTwig() { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + $twig = $this->createMock(Environment::class); $twig->expects($this->exactly(2))->method('render')->willReturn('bar'); $controller = new TemplateController($twig); @@ -36,7 +37,7 @@ public function testTwig() */ public function testTemplating() { - $templating = $this->getMockBuilder(EngineInterface::class)->getMock(); + $templating = $this->createMock(EngineInterface::class); $templating->expects($this->exactly(2))->method('render')->willReturn('bar'); $controller = new TemplateController(null, $templating); @@ -45,12 +46,10 @@ public function testTemplating() $this->assertEquals('bar', $controller('mytemplate')->getContent()); } - /** - * @expectedException \LogicException - * @expectedExceptionMessage You can not use the TemplateController if the Templating Component or the Twig Bundle are not available. - */ public function testNoTwigNorTemplating() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('You can not use the TemplateController if the Templating Component or the Twig Bundle are not available.'); $controller = new TemplateController(); $controller->templateAction('mytemplate')->getContent(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php index 595dfd8d32c92..f3c4d67839be1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Controller/TestController.php @@ -1,10 +1,41 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Controller; use Symfony\Bundle\FrameworkBundle\Controller\Controller; +use Symfony\Bundle\FrameworkBundle\Controller\ControllerTrait; class TestController extends Controller { - use TestControllerTrait; + use ControllerTrait { + generateUrl as public; + redirect as public; + forward as public; + getUser as public; + json as public; + file as public; + isGranted as public; + denyAccessUnlessGranted as public; + redirectToRoute as public; + addFlash as public; + isCsrfTokenValid as public; + renderView as public; + render as public; + stream as public; + createNotFoundException as public; + createAccessDeniedException as public; + createForm as public; + createFormBuilder as public; + getDoctrine as public; + addLink as public; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php index 494cf6f0941b2..e538461ea5994 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolClearerPassTest.php @@ -33,14 +33,17 @@ public function testPoolRefsAreWeak() $container->setParameter('kernel.project_dir', 'foo'); $globalClearer = new Definition(Psr6CacheClearer::class); + $globalClearer->setPublic(true); $container->setDefinition('cache.global_clearer', $globalClearer); $publicPool = new Definition(); + $publicPool->setPublic(true); $publicPool->addArgument('namespace'); $publicPool->addTag('cache.pool', ['clearer' => 'clearer_alias']); $container->setDefinition('public.pool', $publicPool); $publicPool = new Definition(); + $publicPool->setPublic(true); $publicPool->addArgument('namespace'); $publicPool->addTag('cache.pool', ['clearer' => 'clearer_alias', 'name' => 'pool2']); $container->setDefinition('public.pool2', $publicPool); @@ -52,6 +55,7 @@ public function testPoolRefsAreWeak() $container->setDefinition('private.pool', $privatePool); $clearer = new Definition(); + $clearer->setPublic(true); $container->setDefinition('clearer', $clearer); $container->setAlias('clearer_alias', 'clearer'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php index 747e29ffefe89..55a92488d5f2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPassTest.php @@ -26,7 +26,7 @@ class CachePoolPassTest extends TestCase { private $cachePoolPass; - protected function setUp() + protected function setUp(): void { $this->cachePoolPass = new CachePoolPass(); } @@ -111,12 +111,10 @@ public function testWithNameAttribute() $this->assertSame('+naTpPa4Sm', $cachePool->getArgument(1)); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid "cache.pool" tag for service "app.cache_pool": accepted attributes are - */ public function testThrowsExceptionWhenCachePoolTagHasUnknownAttributes() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid "cache.pool" tag for service "app.cache_pool": accepted attributes are'); $container = new ContainerBuilder(); $container->setParameter('kernel.container_class', 'app'); $container->setParameter('kernel.project_dir', 'foo'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php index 58362df6ed8f8..c2494d9cb2ec8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/CachePoolPrunerPassTest.php @@ -17,6 +17,7 @@ use Symfony\Component\Cache\Adapter\PhpFilesAdapter; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Reference; /** @@ -59,12 +60,10 @@ public function testCompilePassIsIgnoredIfCommandDoesNotExist() $this->assertCount($aliasesBefore, $container->getAliases()); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException - * @expectedExceptionMessage Class "Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\NotFound" used for service "pool.not-found" cannot be found. - */ public function testCompilerPassThrowsOnInvalidDefinitionClass() { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('Class "Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler\NotFound" used for service "pool.not-found" cannot be found.'); $container = new ContainerBuilder(); $container->register('console.command.cache_pool_prune')->addArgument([]); $container->register('pool.not-found', NotFound::class)->addTag('cache.pool'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php index 4496c7927e4cc..138a0e4bbc27a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/DataCollectorTranslatorPassTest.php @@ -22,7 +22,7 @@ class DataCollectorTranslatorPassTest extends TestCase private $container; private $dataCollectorTranslatorPass; - protected function setUp() + protected function setUp(): void { $this->container = new ContainerBuilder(); $this->dataCollectorTranslatorPass = new DataCollectorTranslatorPass(); @@ -108,7 +108,7 @@ public function getNotImplementingTranslatorBagInterfaceTranslatorClassNames() class TranslatorWithTranslatorBag implements TranslatorInterface { - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans($id, array $parameters = [], $domain = null, $locale = null): string { } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php index b693165f8b996..17ffbd2347c49 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/ProfilerPassTest.php @@ -24,11 +24,10 @@ class ProfilerPassTest extends TestCase * Thus, a fully-valid tag looks something like this: * * - * - * @expectedException \InvalidArgumentException */ public function testTemplateNoIdThrowsException() { + $this->expectException(\InvalidArgumentException::class); $builder = new ContainerBuilder(); $builder->register('profiler', 'ProfilerClass'); $builder->register('my_collector_service') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.php new file mode 100644 index 0000000000000..afc6f9b4b2577 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/SessionPassTest.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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\SessionPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +class SessionPassTest extends TestCase +{ + public function testProcess() + { + $arguments = [ + new Reference('session.flash_bag'), + new Reference('session.attribu 10000 te_bag'), + ]; + $container = new ContainerBuilder(); + $container + ->register('session') + ->setArguments($arguments); + $container + ->register('session.flash_bag') + ->setFactory([new Reference('session'), 'getFlashBag']); + $container + ->register('session.attribute_bag') + ->setFactory([new Reference('session'), 'getAttributeBag']); + + (new SessionPass())->process($container); + + $this->assertSame($arguments, $container->getDefinition('session')->getArguments()); + $this->assertNull($container->getDefinition('session.flash_bag')->getFactory()); + $this->assertNull($container->getDefinition('session.attribute_bag')->getFactory()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php index 1377a62882494..89a35285ba234 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassTest.php @@ -31,4 +31,27 @@ public function testProcess() $this->assertSame([sprintf('%s: Tag "kenrel.event_subscriber" was defined on service(s) "foo", "bar", but was never used. Did you mean "kernel.event_subscriber"?', UnusedTagsPass::class)], $container->getCompiler()->getLog()); } + + public function testMissingKnownTags() + { + if (\dirname((new \ReflectionClass(ContainerBuilder::class))->getFileName(), 3) !== \dirname(__DIR__, 5)) { + $this->markTestSkipped('Tests are not run from the root symfony/symfony metapackage.'); + } + + $this->assertSame(UnusedTagsPassUtils::getDefinedTags(), $this->getKnownTags(), 'The src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/UnusedTagsPass.php file must be updated; run src/Symfony/Bundle/FrameworkBundle/Resources/bin/check-unused-known-tags.php.'); + } + + private function getKnownTags(): array + { + // get tags in UnusedTagsPass + $target = \dirname(__DIR__, 3).'/DependencyInjection/Compiler/UnusedTagsPass.php'; + $contents = file_get_contents($target); + preg_match('{private \$knownTags = \[(.+?)\];}sm', $contents, $matches); + $tags = array_values(array_filter(array_map(function ($str) { + return trim(preg_replace('{^ +\'(.+)\',}', '$1', $str)); + }, explode("\n", $matches[1])))); + sort($tags); + + return $tags; + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.php new file mode 100644 index 0000000000000..67c97263ccdfd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/UnusedTagsPassUtils.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\Bundle\FrameworkBundle\Tests\DependencyInjection\Compiler; + +use Symfony\Component\Finder\Finder; + +class UnusedTagsPassUtils +{ + public static function getDefinedTags(): array + { + $tags = [ + 'proxy' => true, + ]; + + // get all tags used in XML configs + $files = Finder::create()->files()->name('*.xml')->path('Resources')->notPath('Tests')->in(\dirname(__DIR__, 5)); + foreach ($files as $file) { + $contents = file_get_contents($file); + if (preg_match_all('{files()->name('*.php')->path('DependencyInjection')->notPath('Tests')->in(\dirname(__DIR__, 5)); + foreach ($files as $file) { + $contents = file_get_contents($file); + if (preg_match_all('{findTaggedServiceIds\(\'([^\']+)\'}', $contents, $matches)) { + foreach ($matches[1] as $match) { + if ('my.tag' === $match) { + continue; + } + $tags[$match] = true; + } + } + if (preg_match_all('{findTaggedServiceIds\(\$this->([^,\)]+)}', $contents, $matches)) { + foreach ($matches[1] as $var) { + if (preg_match_all('{\$'.$var.' = \'([^\']+)\'}', $contents, $matches)) { + foreach ($matches[1] as $match) { + $tags[$match] = true; + } + } + } + } + } + + $tags = array_keys($tags); + sort($tags); + + return $tags; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php index 44c87b188fa17..6b12a00bb9389 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Compiler/WorkflowGuardListenerPassTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\WorkflowGuardListenerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\Security\Core\Authentication\AuthenticationTrustResolverInterface; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; @@ -25,7 +26,7 @@ class WorkflowGuardListenerPassTest extends TestCase private $container; private $compilerPass; - protected function setUp() + protected function setUp(): void { $this->container = new ContainerBuilder(); $this->compilerPass = new WorkflowGuardListenerPass(); @@ -52,12 +53,10 @@ public function testNoExeptionIfAllDependenciesArePresent() $this->assertFalse($this->container->hasParameter('workflow.has_guard_listeners')); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage The "security.token_storage" service is needed to be able to use the workflow guard listener. - */ public function testExceptionIfTheTokenStorageServiceIsNotPresent() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "security.token_storage" service is needed to be able to use the workflow guard listener.'); $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); @@ -66,12 +65,10 @@ public function testExceptionIfTheTokenStorageServiceIsNotPresent() $this->compilerPass->process($this->container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage The "security.authorization_checker" service is needed to be able to use the workflow guard listener. - */ public function testExceptionIfTheAuthorizationCheckerServiceIsNotPresent() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "security.authorization_checker" service is needed to be able to use the workflow guard listener.'); $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authentication.trust_resolver', AuthenticationTrustResolverInterface::class); @@ -80,12 +77,10 @@ public function testExceptionIfTheAuthorizationCheckerServiceIsNotPresent() $this->compilerPass->process($this->container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener. - */ public function testExceptionIfTheAuthenticationTrustResolverServiceIsNotPresent() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "security.authentication.trust_resolver" service is needed to be able to use the workflow guard listener.'); $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); @@ -94,12 +89,10 @@ public function testExceptionIfTheAuthenticationTrustResolverServiceIsNotPresent $this->compilerPass->process($this->container); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage The "security.role_hierarchy" service is needed to be able to use the workflow guard listener. - */ public function testExceptionIfTheRoleHierarchyServiceIsNotPresent() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('The "security.role_hierarchy" service is needed to be able to use the workflow guard listener.'); $this->container->setParameter('workflow.has_guard_listeners', true); $this->container->register('security.token_storage', TokenStorageInterface::class); $this->container->register('security.authorization_checker', AuthorizationCheckerInterface::class); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php index 44bc6f8e15b33..861e161f6792d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -63,10 +63,10 @@ public function getTestValidSessionName() /** * @dataProvider getTestInvalidSessionName - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException */ public function testInvalidSessionName($sessionName) { + $this->expectException(InvalidConfigurationException::class); $processor = new Processor(); $processor->processConfiguration( new Configuration(true), @@ -140,12 +140,8 @@ public function provideValidAssetsPackageNameConfigurationTests() */ public function testInvalidAssetsConfiguration(array $assetConfig, $expectedMessage) { - if (method_exists($this, 'expectException')) { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage($expectedMessage); - } else { - $this->setExpectedException(InvalidConfigurationException::class, $expectedMessage); - } + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage($expectedMessage); $processor = new Processor(); $configuration = new Configuration(true); @@ -191,15 +187,102 @@ public function provideInvalidAssetConfigurationTests() yield [$createPackageConfig($config), 'You cannot use both "version" and "json_manifest_path" at the same time under "assets" packages.']; } + /** + * @dataProvider provideValidLockConfigurationTests + */ + public function testValidLockConfiguration($lockConfig, $processedConfig) + { + $processor = new Processor(); + $configuration = new Configuration(true); + $config = $processor->processConfiguration($configuration, [ + [ + 'lock' => $lockConfig, + ], + ]); + + $this->assertArrayHasKey('lock', $config); + + $this->assertEquals($processedConfig, $config['lock']); + } + + public function provideValidLockConfigurationTests() + { + yield [null, ['enabled' => true, 'resources' => ['default' => [class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphore' : 'flock']]]]; + + yield ['flock', ['enabled' => true, 'resources' => ['default' => ['flock']]]]; + yield [['flock', 'semaphore'], ['enabled' => true, 'resources' => ['default' => ['flock', 'semaphore']]]]; + yield [['foo' => 'flock', 'bar' => 'semaphore'], ['enabled' => true, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['foo' => ['flock', 'semaphore'], 'bar' => 'semaphore'], ['enabled' => true, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => ['semaphore']]]]; + yield [['default' => 'flock'], ['enabled' => true, 'resources' => ['default' => ['flock']]]]; + + yield [['enabled' => false, 'flock'], ['enabled' => false, 'resources' => ['default' => ['flock']]]]; + yield [['enabled' => false, ['flock', 'semaphore']], ['enabled' => false, 'resources' => ['default' => ['flock', 'semaphore']]]]; + yield [['enabled' => false, 'foo' => 'flock', 'bar' => 'semaphore'], ['enabled' => false, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['enabled' => false, 'foo' => ['flock', 'semaphore']], ['enabled' => false, 'resources' => ['foo' => ['flock', 'semaphore']]]]; + yield [['enabled' => false, 'default' => 'flock'], ['enabled' => false, 'resources' => ['default' => ['flock']]]]; + + yield [['resources' => 'flock'], ['enabled' => true, 'resources' => ['default' => ['flock']]]]; + yield [['resources' => ['flock', 'semaphore']], ['enabled' => true, 'resources' => ['default' => ['flock', 'semaphore']]]]; + yield [['resources' => ['foo' => 'flock', 'bar' => 'semaphore']], ['enabled' => true, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['resources' => ['foo' => ['flock', 'semaphore'], 'bar' => 'semaphore']], ['enabled' => true, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => ['semaphore']]]]; + yield [['resources' => ['default' => 'flock']], ['enabled' => true, 'resources' => ['default' => ['flock']]]]; + + yield [['enabled' => false, 'resources' => 'flock'], ['enabled' => false, 'resources' => ['default' => ['flock']]]]; + yield [['enabled' => false, 'resources' => ['flock', 'semaphore']], ['enabled' => false, 'resources' => ['default' => ['flock', 'semaphore']]]]; + yield [['enabled' => false, 'resources' => ['foo' => 'flock', 'bar' => 'semaphore']], ['enabled' => false, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['enabled' => false, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => 'semaphore']], ['enabled' => false, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => ['semaphore']]]]; + yield [['enabled' => false, 'resources' => ['default' => 'flock']], ['enabled' => false, 'resources' => ['default' => ['flock']]]]; + + // xml + + yield [['resource' => ['flock']], ['enabled' => true, 'resources' => ['default' => ['flock']]]]; + yield [['resource' => ['flock', ['name' => 'foo', 'value' => 'semaphore']]], ['enabled' => true, 'resources' => ['default' => ['flock'], 'foo' => ['semaphore']]]]; + yield [['resource' => [['name' => 'foo', 'value' => 'flock']]], ['enabled' => true, 'resources' => ['foo' => ['flock']]]]; + yield [['resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'foo', 'value' => 'semaphore']]], ['enabled' => true, 'resources' => ['foo' => ['flock', 'semaphore']]]]; + yield [['resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'bar', 'value' => 'semaphore']]], ['enabled' => true, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'foo', 'value' => 'semaphore'], ['name' => 'bar', 'value' => 'semaphore']]], ['enabled' => true, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => ['semaphore']]]]; + + yield [['enabled' => false, 'resource' => ['flock']], ['enabled' => false, 'resources' => ['default' => ['flock']]]]; + yield [['enabled' => false, 'resource' => ['flock', ['name' => 'foo', 'value' => 'semaphore']]], ['enabled' => false, 'resources' => ['default' => ['flock'], 'foo' => ['semaphore']]]]; + yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'flock']]], ['enabled' => false, 'resources' => ['foo' => ['flock']]]]; + yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'foo', 'value' => 'semaphore']]], ['enabled' => false, 'resources' => ['foo' => ['flock', 'semaphore']]]]; + yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'bar', 'value' => 'semaphore']]], ['enabled' => false, 'resources' => ['foo' => ['flock'], 'bar' => ['semaphore']]]]; + yield [['enabled' => false, 'resource' => [['name' => 'foo', 'value' => 'flock'], ['name' => 'foo', 'value' => 'semaphore'], ['name' => 'bar', 'value' => 'semaphore']]], ['enabled' => false, 'resources' => ['foo' => ['flock', 'semaphore'], 'bar' => ['semaphore']]]]; + } + + public function testLockMergeConfigs() + { + $processor = new Processor(); + $configuration = new Configuration(true); + $config = $processor->processConfiguration($configuration, [ + [ + 'lock' => [ + 'payload' => 'flock', + ], + ], + [ + 'lock' => [ + 'payload' => 'semaphore', + ], + ], + ]); + + $this->assertEquals( + [ + 'enabled' => true, + 'resources' => [ + 'payload' => ['semaphore'], + ], + ], + $config['lock'] + ); + } + public function testItShowANiceMessageIfTwoMessengerBusesAreConfiguredButNoDefaultBus() { $expectedMessage = 'You must specify the "default_bus" if you define more than one bus.'; - if (method_exists($this, 'expectException')) { - $this->expectException(InvalidConfigurationException::class); - $this->expectExceptionMessage($expectedMessage); - } else { - $this->setExpectedException(InvalidConfigurationException::class, $expectedMessage); - } + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage($expectedMessage); $processor = new Processor(); $configuration = new Configuration(true); @@ -216,6 +299,110 @@ public function testItShowANiceMessageIfTwoMessengerBusesAreConfiguredButNoDefau ]); } + public function testBusMiddlewareDontMerge() + { + $processor = new Processor(); + $configuration = new Configuration(true); + $config = $processor->processConfiguration($configuration, [ + [ + 'messenger' => [ + 'default_bus' => 'existing_bus', + 'buses' => [ + 'existing_bus' => [ + 'middleware' => 'existing_bus.middleware', + ], + 'common_bus' => [ + 'default_middleware' => false, + 'middleware' => 'common_bus.old_middleware', + ], + ], + ], + ], + [ + 'messenger' => [ + 'buses' => [ + 'common_bus' => [ + 'middleware' => 'common_bus.new_middleware', + ], + 'new_bus' => [ + 'middleware' => 'new_bus.middleware', + ], + ], + ], + ], + ]); + + $this->assertEquals( + [ + 'existing_bus' => [ + 'default_middleware' => true, + 'middleware' => [ + ['id' => 'existing_bus.middleware', 'arguments' => []], + ], + ], + 'common_bus' => [ + 'default_middleware' => false, + 'middleware' => [ + ['id' => 'common_bus.new_middleware', 'arguments' => []], + ], + ], + 'new_bus' => [ + 'default_middleware' => true, + 'middleware' => [ + ['id' => 'new_bus.middleware', 'arguments' => []], + ], + ], + ], + $config['messenger']['buses'] + ); + } + + public function testItErrorsWhenDefaultBusDoesNotExist() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The specified default bus "foo" is not configured. Available buses are "bar", "baz".'); + + $processor->processConfiguration($configuration, [ + [ + 'messenger' => [ + 'default_bus' => 'foo', + 'buses' => [ + 'bar' => null, + 'baz' => null, + ], + ], + ], + ]); + } + + public function testLockCanBeDisabled() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $config = $processor->processConfiguration($configuration, [ + ['lock' => ['enabled' => false]], + ]); + + $this->assertFalse($config['lock']['enabled']); + } + + public function testEnabledLockNeedsResources() + { + $processor = new Processor(); + $configuration = new Configuration(true); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid configuration for path "framework.lock": At least one resource must be defined.'); + + $processor->processConfiguration($configuration, [ + ['lock' => ['enabled' => true]], + ]); + } + protected static function getBundleDefaultConfig() { return [ @@ -249,6 +436,7 @@ protected static function getBundleDefaultConfig() 'translator' => [ 'enabled' => !class_exists(FullStack::class), 'fallbacks' => [], + 'cache_dir' => '%kernel.cache_dir%/translations', 'logging' => false, 'formatter' => 'translator.formatter.default', 'paths' => [], @@ -301,6 +489,7 @@ protected static function getBundleDefaultConfig() 'cookie_httponly' => true, 'cookie_samesite' => null, 'gc_probability' => 1, + 'save_path' => '%kernel.cache_dir%/sessions', 'metadata_update_threshold' => 0, ], 'request' => [ @@ -375,9 +564,17 @@ class_exists(SemaphoreStore::class) && SemaphoreStore::isSupported() ? 'semaphor 'scoped_clients' => [], ], 'mailer' => [ - 'dsn' => 'smtp://null', + 'dsn' => null, + 'transports' => [], 'enabled' => !class_exists(FullStack::class) && class_exists(Mailer::class), ], + 'error_controller' => 'error_controller', + 'secrets' => [ + 'enabled' => true, + 'vault_directory' => '%kernel.project_dir%/config/secrets/%kernel.environment%', + 'local_dotenv_file' => '%kernel.project_dir%/.env.%kernel.environment%.local', + 'decryption_env_var' => 'base64:default::SYMFONY_DECRYPTION_SECRET', + ], ]; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/CustomPathBundle/src/CustomPathBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/CustomPathBundle/src/CustomPathBundle.php index 166b606a459e2..f1f2bdc746e9e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/CustomPathBundle/src/CustomPathBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/CustomPathBundle/src/CustomPathBundle.php @@ -9,13 +9,13 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests; +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\CustomPathBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; class CustomPathBundle extends Bundle { - public function getPath() + public function getPath(): string { return __DIR__.'/..'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/TestBundle/TestBundle.php index 2f090b2de8d53..c58b25066bf4a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/TestBundle/TestBundle.php @@ -9,7 +9,7 @@ * file that was distributed with this source code. */ -namespace Symfony\Bundle\FrameworkBundle\Tests; +namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\Fixtures\TestBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php index 8d92edf766924..5e607dfdbbfca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache.php @@ -32,6 +32,11 @@ 'redis://foo' => 'cache.adapter.redis', ], ], + 'cache.ccc' => [ + 'adapter' => 'cache.adapter.array', + 'default_lifetime' => 410, + 'tags' => true, + ], ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php deleted file mode 100644 index 4f819e7204d71..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/cache_env_var.php +++ /dev/null @@ -1,9 +0,0 @@ -setParameter('env(REDIS_URL)', 'redis://paas.com'); - -$container->loadFromExtension('framework', [ - 'cache' => [ - 'default_redis_provider' => '%env(REDIS_URL)%', - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_disabled.php new file mode 100644 index 0000000000000..bd482c48de63c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/form_csrf_disabled.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'csrf_protection' => false, + 'form' => [ + 'csrf_protection' => true, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php new file mode 100644 index 0000000000000..dbcf5b786dba9 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/fragments_and_hinclude.php @@ -0,0 +1,8 @@ +loadFromExtension('framework', [ + 'fragments' => [ + 'enabled' => true, + 'hinclude_default_template' => 'global_hinclude_template', + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php index e02ba9183f5e6..e633d34187cf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -37,6 +37,8 @@ 'gc_maxlifetime' => 90000, 'gc_divisor' => 108, 'gc_probability' => 1, + 'sid_length' => 22, + 'sid_bits_per_character' => 4, 'save_path' => '/path/to/sessions', ], 'assets' => [ @@ -46,6 +48,7 @@ 'enabled' => true, 'fallback' => 'fr', 'paths' => ['%kernel.project_dir%/Fixtures/translations'], + 'cache_dir' => '%kernel.cache_dir%/translations', ], 'validation' => [ 'enabled' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php index 04a227c24cb14..cf83e31af2f02 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_full_default_options.php @@ -9,6 +9,7 @@ 'resolve' => ['localhost' => '127.0.0.1'], 'proxy' => 'proxy.org', 'timeout' => 3.5, + 'max_duration' => 10.1, 'bindto' => '127.0.0.1', 'verify_peer' => true, 'verify_host' => true, diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_scoped_without_query_option.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_scoped_without_query_option.php new file mode 100644 index 0000000000000..0d3dc88472f84 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_scoped_without_query_option.php @@ -0,0 +1,11 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'scoped_clients' => [ + 'foo' => [ + 'scope' => '.*', + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_xml_key.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_xml_key.php new file mode 100644 index 0000000000000..64778c61561b6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/http_client_xml_key.php @@ -0,0 +1,22 @@ +loadFromExtension('framework', [ + 'http_client' => [ + 'default_options' => [ + 'resolve' => [ + 'host' => '127.0.0.1', + ], + ], + 'scoped_clients' => [ + 'foo' => [ + 'base_uri' => 'http://example.com', + 'query' => [ + 'key' => 'foo', + ], + 'resolve' => [ + 'host' => '127.0.0.1', + ], + ], + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php new file mode 100644 index 0000000000000..7eec06a9a0e50 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_dsn.php @@ -0,0 +1,15 @@ +extension('framework', [ + 'mailer' => [ + 'dsn' => 'smtp://example.com', + 'envelope' => [ + 'sender' => 'sender@example.org', + 'recipients' => ['redirected@example.org', 'redirected1@example.org'], + ], + ], + ]); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php new file mode 100644 index 0000000000000..1bc79f3dd204c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/mailer_with_transports.php @@ -0,0 +1,18 @@ +extension('framework', [ + 'mailer' => [ + 'transports' => [ + 'transport1' => 'smtp://example1.com', + 'transport2' => 'smtp://example2.com', + ], + 'envelope' => [ + 'sender' => 'sender@example.org', + 'recipients' => ['redirected@example.org', 'redirected1@example.org'], + ], + ], + ]); +}; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php index 1160dfc573a7c..adb8239d04737 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger.php @@ -9,5 +9,10 @@ FooMessage::class => ['sender.bar', 'sender.biz'], BarMessage::class => 'sender.foo', ], + 'transports' => [ + 'sender.biz' => 'null://', + 'sender.bar' => 'null://', + 'sender.foo' => 'null://', + ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php new file mode 100644 index 0000000000000..e02542d9778c6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_disabled.php @@ -0,0 +1,5 @@ +loadFromExtension('framework', [ + 'messenger' => false, +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php index 9badc0650597c..eb459509015dd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing.php @@ -7,14 +7,15 @@ 'default_serializer' => 'messenger.transport.symfony_serializer', ], 'routing' => [ - 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage' => ['amqp', 'audit'], - 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage' => [ + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => ['amqp', 'messenger.transport.audit'], + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage' => [ 'senders' => ['amqp', 'audit'], ], '*' => 'amqp', ], 'transports' => [ 'amqp' => 'amqp://localhost/%2f/messages', + 'audit' => 'null://', ], ], ]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php new file mode 100644 index 0000000000000..ee77e3a3f2dbf --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_invalid_transport.php @@ -0,0 +1,16 @@ +loadFromExtension('framework', [ + 'serializer' => true, + 'messenger' => [ + 'serializer' => [ + 'default_serializer' => 'messenger.transport.symfony_serializer', + ], + 'routing' => [ + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => 'invalid', + ], + 'transports' => [ + 'amqp' => 'amqp://localhost/%2f/messages', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php new file mode 100644 index 0000000000000..e58814589b870 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_routing_single.php @@ -0,0 +1,12 @@ +loadFromExtension('framework', [ + 'messenger' => [ + 'routing' => [ + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage' => ['amqp'], + ], + 'transports' => [ + 'amqp' => 'amqp://localhost/%2f/messages', + ], + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php index 68ff3607465b2..0aff440e855e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/messenger_transports.php @@ -3,6 +3,7 @@ $container->loadFromExtension('framework', [ 'serializer' => true, 'messenger' => [ + 'failure_transport' => 'failed', 'serializer' => [ 'default_serializer' => 'messenger.transport.symfony_serializer', ], @@ -12,7 +13,14 @@ 'dsn' => 'amqp://localhost/%2f/messages?exchange_name=exchange_name', 'options' => ['queue' => ['name' => 'Queue']], 'serializer' => 'messenger.transport.native_php_serializer', + 'retry_strategy' => [ + 'max_retries' => 10, + 'delay' => 7, + 'multiplier' => 3, + 'max_delay' => 100, + ], ], + 'failed' => 'in-memory:///', 'redis' => 'redis://127.0.0.1:6379/messages', ], ], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_savepath.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_savepath.php deleted file mode 100644 index 89841bca43d51..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/session_savepath.php +++ /dev/null @@ -1,8 +0,0 @@ -loadFromExtension('framework', [ - 'session' => [ - 'handler_id' => null, - 'save_path' => '/some/path', - ], -]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/translator_cache_dir_disabled.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/translator_cache_dir_disabled.php new file mode 100644 index 0000000000000..6f2568ffd511e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/translator_cache_dir_disabled.php @@ -0,0 +1,7 @@ +loadFromExtension('framework', [ + 'translator' => [ + 'cache_dir' => null, + ], +]); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php index 7c7f7ed0b45f2..995fabffe38b7 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/php/workflows.php @@ -10,6 +10,10 @@ FrameworkExtensionTest::class, ], 'initial_marking' => ['draft'], + 'metadata' => [ + 'title' => 'article workflow', + 'description' => 'workflow for articles', + ], 'places' => [ 'draft', 'wait_for_journalist', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml index 2db74964b53e7..d53e0764f8db3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache.xml @@ -17,6 +17,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml deleted file mode 100644 index 81c96b3a7ff85..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/cache_env_var.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - redis://paas.com - - - - - %env(REDIS_URL)% - - - diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_disabled.xml new file mode 100644 index 0000000000000..e2b7167c84238 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/form_csrf_disabled.xml @@ -0,0 +1,17 @@ + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml new file mode 100644 index 0000000000000..fb007313b9a71 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/fragments_and_hinclude.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 905c187ef8857..8c4c489ea3430 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -15,7 +15,7 @@ - + text/csv @@ -26,7 +26,7 @@ - + %kernel.project_dir%/Fixtures/translations diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml index 2ea78874d2176..aaee419433818 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_full_default_options.xml @@ -11,6 +11,7 @@ proxy="proxy.org" bindto="127.0.0.1" timeout="3.5" + max-duration="10.1" verify-peer="true" max-redirects="2" http-version="2.0" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_scoped_without_query_option.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_scoped_without_query_option.xml new file mode 100644 index 0000000000000..43043aeda2a01 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_scoped_without_query_option.xml @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_xml_key.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_xml_key.xml new file mode 100644 index 0000000000000..95ef0737f8a02 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/http_client_xml_key.xml @@ -0,0 +1,19 @@ + + + + + + + 127.0.0.1 + + + foo + 127.0.0.1 + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer.xml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_dsn.xml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml new file mode 100644 index 0000000000000..a6eb67dc81024 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/mailer_with_transports.xml @@ -0,0 +1,20 @@ + + + + + + + smtp://example1.com + smtp://example2.com + + sender@example.org + redirected@example.org + redirected1@example.org + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml index e0dc11360a7d9..bacd772dcb6fc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger.xml @@ -14,6 +14,9 @@ + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml new file mode 100644 index 0000000000000..6f57398b30d2b --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_disabled.xml @@ -0,0 +1,11 @@ + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml index 43be6fc709d00..0b022e78a0c8c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing.xml @@ -9,11 +9,11 @@ - + - + - + @@ -21,6 +21,7 @@ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml new file mode 100644 index 0000000000000..98c487fbf8bfa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_invalid_transport.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml new file mode 100644 index 0000000000000..349a3728d3935 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_routing_single.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml index bb698cbc17105..837db14c1cad4 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/messenger_transports.xml @@ -7,7 +7,7 @@ - + @@ -16,7 +16,9 @@ Queue + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_savepath.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/translator_cache_dir_disabled.xml similarity index 78% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_savepath.xml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/translator_cache_dir_disabled.xml index a9ddd8016b879..5704ff7cd7ddb 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/session_savepath.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/translator_cache_dir_disabled.xml @@ -6,7 +6,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/symfony https://symfony.com/schema/dic/symfony/symfony-1.0.xsd"> - - + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml index 0c6a638df45cc..290ab50e7d8da 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/xml/workflows.xml @@ -35,6 +35,10 @@ approved_by_spellchecker published + + article workflow + workflow for articles + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml index ee20bc74b22d6..c6c6383715744 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache.yml @@ -23,3 +23,7 @@ framework: - cache.adapter.array - cache.adapter.filesystem - {name: cache.adapter.redis, provider: 'redis://foo'} + cache.ccc: + adapter: cache.adapter.array + default_lifetime: 410 + tags: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml deleted file mode 100644 index 1d9ce5f7f02f7..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/cache_env_var.yml +++ /dev/null @@ -1,6 +0,0 @@ -parameters: - env(REDIS_URL): redis://paas.com - -framework: - cache: - default_redis_provider: "%env(REDIS_URL)%" diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_disabled.yml new file mode 100644 index 0000000000000..9319019c8641a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/form_csrf_disabled.yml @@ -0,0 +1,4 @@ +framework: + csrf_protection: false + form: + csrf_protection: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/fragments_and_hinclude.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyIn 10000 jection/Fixtures/yml/fragments_and_hinclude.yml new file mode 100644 index 0000000000000..b03f37da7946e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/fragments_and_hinclude.yml @@ -0,0 +1,4 @@ +framework: + fragments: + enabled: true + hinclude_default_template: global_hinclude_template diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index 9194911b063c5..a189f992daf34 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -29,6 +29,8 @@ framework: gc_probability: 1 gc_divisor: 108 gc_maxlifetime: 90000 + sid_length: 22 + sid_bits_per_character: 4 save_path: /path/to/sessions assets: version: v1 @@ -36,6 +38,7 @@ framework: enabled: true fallback: fr default_path: '%kernel.project_dir%/translations' + cache_dir: '%kernel.cache_dir%/translations' paths: ['%kernel.project_dir%/Fixtures/translations'] validation: enabled: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml index 5993be1778fe6..ba3aa46259b46 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_full_default_options.yml @@ -8,6 +8,7 @@ framework: resolve: {'localhost': '127.0.0.1'} proxy: proxy.org timeout: 3.5 + max_duration: 10.1 bindto: 127.0.0.1 verify_peer: true verify_host: true diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_scoped_without_query_option.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_scoped_without_query_option.yml new file mode 100644 index 0000000000000..ecfc9d41fd4c3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_scoped_without_query_option.yml @@ -0,0 +1,5 @@ +framework: + http_client: + scoped_clients: + foo: + scope: '.*' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_xml_key.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_xml_key.yml new file mode 100644 index 0000000000000..dc87555a901ae --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/http_client_xml_key.yml @@ -0,0 +1,12 @@ +framework: + http_client: + default_options: + resolve: + host: 127.0.0.1 + scoped_clients: + foo: + base_uri: http://example.com + query: + key: foo + resolve: + host: 127.0.0.1 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml similarity index 100% rename from src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer.yml rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_dsn.yml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml new file mode 100644 index 0000000000000..6035988d76e59 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/mailer_with_transports.yml @@ -0,0 +1,10 @@ +framework: + mailer: + transports: + transport1: 'smtp://example1.com' + transport2: 'smtp://example2.com' + envelope: + sender: sender@example.org + recipients: + - redirected@example.org + - redirected1@example.org diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml index 7f038af11fdff..82fea3b27af23 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger.yml @@ -3,3 +3,7 @@ framework: routing: 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\FooMessage': ['sender.bar', 'sender.biz'] 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\BarMessage': 'sender.foo' + transports: + sender.biz: 'null://' + sender.bar: 'null://' + sender.foo: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml new file mode 100644 index 0000000000000..1b2d2d1a4f475 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_disabled.yml @@ -0,0 +1,2 @@ +framework: + messenger: false diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml index ae060529ffcc3..0e493eb882a02 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing.yml @@ -4,9 +4,10 @@ framework: serializer: default_serializer: messenger.transport.symfony_serializer routing: - 'Symfony\Component\Messenger\Tests\Fixtures\DummyMessage': [amqp, audit] - 'Symfony\Component\Messenger\Tests\Fixtures\SecondMessage': + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp, messenger.transport.audit] + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\SecondMessage': senders: [amqp, audit] '*': amqp transports: amqp: 'amqp://localhost/%2f/messages' + audit: 'null://' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml new file mode 100644 index 0000000000000..3bf0f2ddf9c03 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_invalid_transport.yml @@ -0,0 +1,9 @@ +framework: + serializer: true + messenger: + serializer: + default_serializer: messenger.transport.symfony_serializer + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': invalid + transports: + amqp: 'amqp://localhost/%2f/messages' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml new file mode 100644 index 0000000000000..caa88641887c7 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_routing_single.yml @@ -0,0 +1,7 @@ +framework: + messenger: + routing: + 'Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage': [amqp] + + transports: + amqp: 'amqp://localhost/%2f/messages' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml index 2fc1f482653e4..daab75bd87e40 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/messenger_transports.yml @@ -1,6 +1,7 @@ framework: serializer: true messenger: + failure_transport: failed serializer: default_serializer: messenger.transport.symfony_serializer transports: @@ -11,4 +12,10 @@ framework: queue: name: Queue serializer: 'messenger.transport.native_php_serializer' + retry_strategy: + max_retries: 10 + delay: 7 + multiplier: 3 + max_delay: 100 + failed: 'in-memory:///' redis: 'redis://127.0.0.1:6379/messages' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_savepath.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_savepath.yml deleted file mode 100644 index 174ebe586947e..0000000000000 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/session_savepath.yml +++ /dev/null @@ -1,4 +0,0 @@ -framework: - session: - handler_id: null - save_path: /some/path diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/translator_cache_dir_disabled.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/translator_cache_dir_disabled.yml new file mode 100644 index 0000000000000..6ad1c7330f965 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/translator_cache_dir_disabled.yml @@ -0,0 +1,3 @@ +framework: + translator: + cache_dir: ~ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml index 225106383d1fd..e4ac9c01890e3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/Fixtures/yml/workflows.yml @@ -5,6 +5,9 @@ framework: supports: - Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection\FrameworkExtensionTest initial_marking: [draft] + metadata: + title: article workflow + description: workflow for articles places: # simple format - draft diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php index 1a5e3da1878f3..38e502db56b46 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/FrameworkExtensionTest.php @@ -12,9 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\DependencyInjection; use Doctrine\Common\Annotations\Annotation; +use Doctrine\Common\Annotations\PsrCachedReader; use Psr\Log\LoggerAwareInterface; use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\AddAnnotationsCachedReaderPass; use Symfony\Bundle\FrameworkBundle\DependencyInjection\FrameworkExtension; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Bundle\FullStack; use Symfony\Component\Cache\Adapter\AdapterInterface; @@ -25,9 +27,9 @@ use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter; +use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\DependencyInjection\CachePoolPass; -use Symfony\Component\Config\Resource\DirectoryResource; -use Symfony\Component\Config\Resource\FileExistenceResource; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\ResolveInstanceofConditionalsPass; @@ -35,14 +37,15 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Loader\ClosureLoader; -use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; +use Symfony\Component\DependencyInjection\ParameterBag\EnvPlaceholderParameterBag; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\Form\Form; use Symfony\Component\HttpClient\ScopingHttpClient; use Symfony\Component\HttpKernel\DependencyInjection\LoggerPass; -use Symfony\Component\Messenger\Tests\Fixtures\DummyMessage; use Symfony\Component\Messenger\Transport\TransportFactory; use Symfony\Component\PropertyAccess\PropertyAccessor; +use Symfony\Component\Security\Core\Security; use Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader; use Symfony\Component\Serializer\Mapping\Loader\XmlFileLoader; use Symfony\Component\Serializer\Mapping\Loader\YamlFileLoader; @@ -53,11 +56,15 @@ use Symfony\Component\Serializer\Normalizer\JsonSerializableNormalizer; use Symfony\Component\Serializer\Serializer; use Symfony\Component\Translation\DependencyInjection\TranslatorPass; -use Symfony\Component\Translation\TranslatorInterface; use Symfony\Component\Validator\DependencyInjection\AddConstraintValidatorsPass; use Symfony\Component\Validator\Mapping\Loader\PropertyInfoLoader; use Symfony\Component\Validator\Util\LegacyTranslatorProxy; +use Symfony\Component\Validator\Validation; +use Symfony\Component\Validator\Validator\ValidatorInterface; use Symfony\Component\Workflow; +use Symfony\Component\Workflow\Exception\InvalidDefinitionException; +use Symfony\Component\Workflow\Metadata\InMemoryMetadataStore; +use Symfony\Contracts\Translation\TranslatorInterface; abstract class FrameworkExtensionTest extends TestCase { @@ -77,6 +84,14 @@ public function testFormCsrfProtection() $this->assertEquals('%form.type_extension.csrf.field_name%', $def->getArgument(2)); } + public function testFormCsrfProtectionWithCsrfDisabled() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('To use form CSRF protection, "framework.csrf_protection" must be enabled.'); + + $this->createContainerFromFile('form_csrf_disabled'); + } + public function testPropertyAccessWithDefaultValue() { $container = $this->createContainerFromFile('full'); @@ -101,7 +116,9 @@ public function testPropertyAccessCache() $container = $this->createContainerFromFile('property_accessor'); if (!method_exists(PropertyAccessor::class, 'createCache')) { - return $this->assertFalse($container->hasDefinition('cache.property_access')); + $this->assertFalse($container->hasDefinition('cache.property_access')); + + return; } $cache = $container->getDefinition('cache.property_access'); @@ -114,7 +131,9 @@ public function testPropertyAccessCacheWithDebug() $container = $this->createContainerFromFile('property_accessor', ['kernel.debug' => true]); if (!method_exists(PropertyAccessor::class, 'createCache')) { - return $this->assertFalse($container->hasDefinition('cache.property_access')); + $this->assertFalse($container->hasDefinition('cache.property_access')); + + return; } $cache = $container->getDefinition('cache.property_access'); @@ -122,12 +141,10 @@ public function testPropertyAccessCacheWithDebug() $this->assertSame(ArrayAdapter::class, $cache->getClass(), 'ArrayAdapter should be used in debug mode'); } - /** - * @expectedException \LogicException - * @expectedExceptionMessage CSRF protection needs sessions to be enabled. - */ public function testCsrfProtectionNeedsSessionToBeEnabled() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('CSRF protection needs sessions to be enabled.'); $this->createContainerFromFile('csrf_needs_session'); } @@ -163,13 +180,20 @@ public function testEsiDisabled() /** * @group legacy - * @expectedException \LogicException */ public function testAmbiguousWhenBothTemplatingAndFragments() { + $this->expectException(\LogicException::class); $this->createContainerFromFile('template_and_fragments'); } + public function testFragmentsAndHinclude() + { + $container = $this->createContainerFromFile('fragments_and_hinclude'); + $this->assertTrue($container->hasParameter('fragment.renderer.hinclude.global_template')); + $this->assertEquals('global_hinclude_template', $container->getParameter('fragment.renderer.hinclude.global_template')); + } + public function testSsi() { $container = $this->createContainerFromFile('full'); @@ -235,6 +259,12 @@ public function testWorkflows() ); $this->assertCount(4, $workflowDefinition->getArgument(1)); $this->assertSame(['draft'], $workflowDefinition->getArgument(2)); + $metadataStoreDefinition = $workflowDefinition->getArgument(3); + $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + $this->assertSame([ + 'title' => 'article workflow', + 'description' => 'workflow for articles', + ], $metadataStoreDefinition->getArgument(0)); $this->assertTrue($container->hasDefinition('state_machine.pull_request'), 'State machine is registered as a service'); $this->assertSame('state_machine.abstract', $container->getDefinition('state_machine.pull_request')->getParent()); @@ -259,7 +289,7 @@ public function testWorkflows() $metadataStoreDefinition = $stateMachineDefinition->getArgument(3); $this->assertInstanceOf(Definition::class, $metadataStoreDefinition); - $this->assertSame(Workflow\Metadata\InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); + $this->assertSame(InMemoryMetadataStore::class, $metadataStoreDefinition->getClass()); $workflowMetadata = $metadataStoreDefinition->getArgument(0); $this->assertSame(['title' => 'workflow title'], $workflowMetadata); @@ -315,49 +345,41 @@ public function testWorkflowLegacy() $this->assertSame(['workflow.definition' => [['name' => 'legacy', 'type' => 'state_machine']]], $workflowDefinition->getTags()); } - /** - * @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException - * @expectedExceptionMessage A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" where found on StateMachine "my_workflow". - */ public function testWorkflowAreValidated() { + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "go" from place/state "first" were found on StateMachine "my_workflow".'); $this->createContainerFromFile('workflow_not_valid'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "type" and "service" cannot be used together. - */ public function testWorkflowCannotHaveBothTypeAndService() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"type" and "service" cannot be used together.'); $this->createContainerFromFile('workflow_legacy_with_type_and_service'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "supports" and "support_strategy" cannot be used together. - */ public function testWorkflowCannotHaveBothSupportsAndSupportStrategy() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"supports" and "support_strategy" cannot be used together.'); $this->createContainerFromFile('workflow_with_support_and_support_strategy'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "supports" or "support_strategy" should be configured. - */ public function testWorkflowShouldHaveOneOfSupportsAndSupportStrategy() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"supports" or "support_strategy" should be configured.'); $this->createContainerFromFile('workflow_without_support_and_support_strategy'); } /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "arguments" and "service" cannot be used together. * @group legacy */ public function testWorkflowCannotHaveBothArgumentsAndService() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"arguments" and "service" cannot be used together.'); $this->createContainerFromFile('workflow_legacy_with_arguments_and_service'); } @@ -516,11 +538,9 @@ public function testRouter() $this->assertEquals('xml', $arguments[2]['resource_type'], '->registerRouterConfiguration() sets routing resource type'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - */ public function testRouterRequiresResourceOption() { + $this->expectException(InvalidConfigurationException::class); $container = $this->createContainer(); $loader = new FrameworkExtension(); $loader->load([['router' => true]], $container); @@ -546,6 +566,8 @@ public function testSession() $this->assertEquals(108, $options['gc_divisor']); $this->assertEquals(1, $options['gc_probability']); $this->assertEquals(90000, $options['gc_maxlifetime']); + $this->assertEquals(22, $options['sid_length']); + $this->assertEquals(4, $options['sid_bits_per_character']); $this->assertEquals('/path/to/sessions', $container->getParameter('session.save_path')); } @@ -557,19 +579,12 @@ public function testNullSessionHandler() $this->assertTrue($container->hasDefinition('session'), '->registerSessionConfiguration() loads session.xml'); $this->assertNull($container->getDefinition('session.storage.native')->getArgument(1)); $this->assertNull($container->getDefinition('session.storage.php_bridge')->getArgument(0)); + $this->assertSame('session.handler.native_file', (string) $container->getAlias('session.handler')); $expected = ['session', 'initialized_session']; $this->assertEquals($expected, array_keys($container->getDefinition('session_listener')->getArgument(0)->getValues())); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - */ - public function testNullSessionHandlerWithSavePath() - { - $this->createContainerFromFile('session_savepath'); - } - public function testRequest() { $container = $this->createContainerFromFile('full'); @@ -677,15 +692,29 @@ public function testWebLink() $this->assertTrue($container->hasDefinition('web_link.add_link_header_listener')); } + public function testMessengerServicesRemovedWhenDisabled() + { + $container = $this->createContainerFromFile('messenger_disabled'); + $this->assertFalse($container->hasDefinition('console.command.messenger_consume_messages')); + $this->assertFalse($container->hasDefinition('console.command.messenger_debug')); + $this->assertFalse($container->hasDefinition('console.command.messenger_stop_workers')); + $this->assertFalse($container->hasDefinition('console.command.messenger_setup_transports')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_retry')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_show')); + $this->assertFalse($container->hasDefinition('console.command.messenger_failed_messages_remove')); + $this->assertFalse($container->hasDefinition('cache.messenger.restart_workers_signal')); + } + public function testMessenger() { $container = $this->createContainerFromFile('messenger'); + $this->assertTrue($container->hasDefinition('console.command.messenger_consume_messages')); $this->assertTrue($container->hasAlias('message_bus')); $this->assertTrue($container->getAlias('message_bus')->isPublic()); $this->assertTrue($container->hasAlias('messenger.default_bus')); $this->assertTrue($container->getAlias('messenger.default_bus')->isPublic()); - $this->assertFalse($container->hasDefinition('messenger.transport.amqp.factory')); - $this->assertFalse($container->hasDefinition('messenger.transport.redis.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); + $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); $this->assertTrue($container->hasDefinition('messenger.transport_factory')); $this->assertSame(TransportFactory::class, $container->getDefinition('messenger.transport_factory')->getClass()); } @@ -706,7 +735,7 @@ public function testMessengerTransports() $this->assertEquals([new Reference('messenger.transport_factory'), 'createTransport'], $transportFactory); $this->assertCount(3, $transportArguments); $this->assertSame('amqp://localhost/%2f/messages?exchange_name=exchange_name', $transportArguments[0]); - $this->assertEquals(['queue' => ['name' => 'Queue']], $transportArguments[1]); + $this->assertEquals(['queue' => ['name' => 'Queue'], 'transport_name' => 'customised'], $transportArguments[1]); $this->assertEquals(new Reference('messenger.transport.native_php_serializer'), $transportArguments[2]); $this->assertTrue($container->hasDefinition('messenger.transport.amqp.factory')); @@ -720,6 +749,13 @@ public function testMessengerTransports() $this->assertSame('redis://127.0.0.1:6379/messages', $transportArguments[0]); $this->assertTrue($container->hasDefinition('messenger.transport.redis.factory')); + + $this->assertSame(10, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(0)); + $this->assertSame(7, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(1)); + $this->assertSame(3, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(2)); + $this->assertSame(100, $container->getDefinition('messenger.retry.multiplier_retry_strategy.customised')->getArgument(3)); + + $this->assertEquals(new Reference('messenger.transport.failed'), $container->getDefinition('messenger.failure.send_failed_message_to_failure_transport_listener')->getArgument(0)); } public function testMessengerRouting() @@ -728,14 +764,20 @@ public function testMessengerRouting() $senderLocatorDefinition = $container->getDefinition('messenger.senders_locator'); $sendersMapping = $senderLocatorDefinition->getArgument(0); - $this->assertEquals([ - 'amqp', - 'audit', - ], $sendersMapping[DummyMessage::class]); + $this->assertEquals(['amqp', 'messenger.transport.audit'], $sendersMapping[DummyMessage::class]); $sendersLocator = $container->getDefinition((string) $senderLocatorDefinition->getArgument(1)); - $this->assertSame(['amqp', 'audit'], array_keys($sendersLocator->getArgument(0))); + $this->assertSame(['amqp', 'audit', 'messenger.transport.amqp', 'messenger.transport.audit'], array_keys($sendersLocator->getArgument(0))); $this->assertEquals(new Reference('messenger.transport.amqp'), $sendersLocator->getArgument(0)['amqp']->getValues()[0]); - $this->assertEquals(new Reference('audit'), $sendersLocator->getArgument(0)['audit']->getValues()[0]); + $this->assertEquals(new Reference('messenger.transport.audit'), $sendersLocator->getArgument(0)['messenger.transport.audit']->getValues()[0]); + } + + public function testMessengerRoutingSingle() + { + $container = $this->createContainerFromFile('messenger_routing_single'); + $senderLocatorDefinition = $container->getDefinition('messenger.senders_locator'); + + $sendersMapping = $senderLocatorDefinition->getArgument(0); + $this->assertEquals(['amqp'], $sendersMapping[DummyMessage::class]); } public function testMessengerTransportConfiguration() @@ -757,6 +799,7 @@ public function testMessengerWithMultipleBuses() $this->assertSame([], $container->getDefinition('messenger.bus.commands')->getArgument(0)); $this->assertEquals([ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.commands']], + ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ['id' => 'send_message'], @@ -766,6 +809,7 @@ public function testMessengerWithMultipleBuses() $this->assertSame([], $container->getDefinition('messenger.bus.events')->getArgument(0)); $this->assertEquals([ ['id' => 'add_bus_name_stamp_middleware', 'arguments' => ['messenger.bus.events']], + ['id' => 'reject_redelivered_message_middleware'], ['id' => 'dispatch_after_current_bus'], ['id' => 'failed_message_processing_middleware'], ['id' => 'with_factory', 'arguments' => ['foo', true, ['bar' => 'baz']]], @@ -785,15 +829,20 @@ public function testMessengerWithMultipleBuses() $this->assertSame('messenger.bus.commands', (string) $container->getAlias('messenger.default_bus')); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, {"foo":["qux"],"bar":["baz"]} given. - */ public function testMessengerMiddlewareFactoryErroneousFormat() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid middleware at path "framework.messenger": a map with a single factory id as key and its arguments as value was expected, {"foo":["qux"],"bar":["baz"]} given.'); $this->createContainerFromFile('messenger_middleware_factory_erroneous_format'); } + public function testMessengerInvalidTransportRouting() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Invalid Messenger routing configuration: the "Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Messenger\DummyMessage" class is being routed to a sender called "invalid". This is not a valid transport or service id.'); + $this->createContainerFromFile('messenger_routing_invalid_transport'); + } + public function testTranslator() { $container = $this->createContainerFromFile('full'); @@ -801,20 +850,23 @@ public function testTranslator() $this->assertEquals('translator.default', (string) $container->getAlias('translator'), '->registerTranslatorConfiguration() redefines translator service from identity to real translator'); $options = $container->getDefinition('translator.default')->getArgument(4); + $this->assertArrayHasKey('cache_dir', $options); + $this->assertSame($container->getParameter('kernel.cache_dir').'/translations', $options['cache_dir']); + $files = array_map('realpath', $options['resource_files']['en']); - $ref = new \ReflectionClass('Symfony\Component\Validator\Validation'); + $ref = new \ReflectionClass(Validation::class); $this->assertContains( strtr(\dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', \DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds Validator translation resources' ); - $ref = new \ReflectionClass('Symfony\Component\Form\Form'); + $ref = new \ReflectionClass(Form::class); $this->assertContains( strtr(\dirname($ref->getFileName()).'/Resources/translations/validators.en.xlf', '/', \DIRECTORY_SEPARATOR), $files, '->registerTranslatorConfiguration() finds Form translation resources' ); - $ref = new \ReflectionClass('Symfony\Component\Security\Core\Security'); + $ref = new \ReflectionClass(Security::class); $this->assertContains( strtr(\dirname($ref->getFileName()).'/Resources/translations/security.en.xlf', '/', \DIRECTORY_SEPARATOR), $files, @@ -835,6 +887,19 @@ public function testTranslator() $files, '->registerTranslatorConfiguration() finds translation resources with dots in domain' ); + $this->assertContains(strtr(__DIR__.'/translations/security.en.yaml', '/', \DIRECTORY_SEPARATOR), $files); + + $positionOverridingTranslationFile = array_search(strtr(realpath(__DIR__.'/translations/security.en.yaml'), '/', \DIRECTORY_SEPARATOR), $files); + + if (false !== $positionCoreTranslationFile = array_search(strtr(realpath(__DIR__.'/../../../../Component/Security/Core/Resources/translations/security.en.xlf'), '/', \DIRECTORY_SEPARATOR), $files)) { + $this->assertContains(strtr(realpath(__DIR__.'/../../../../Component/Security/Core/Resources/translations/security.en.xlf'), '/', \DIRECTORY_SEPARATOR), $files); + } else { + $this->assertContains(strtr(realpath(__DIR__.'/../../vendor/symfony/security-core/Resources/translations/security.en.xlf'), '/', \DIRECTORY_SEPARATOR), $files); + + $positionCoreTranslationFile = array_search(strtr(realpath(__DIR__.'/../../vendor/symfony/security-core/Resources/translations/security.en.xlf'), '/', \DIRECTORY_SEPARATOR), $files); + } + + $this->assertGreaterThan($positionCoreTranslationFile, $positionOverridingTranslationFile); $calls = $container->getDefinition('translator.default')->getMethodCalls(); $this->assertEquals(['fr'], $calls[1][1][0]); @@ -848,16 +913,7 @@ function ($directory) { $this->assertNotEmpty($nonExistingDirectories, 'FrameworkBundle should pass non existing directories to Translator'); - $resources = $container->getResources(); - foreach ($resources as $resource) { - if ($resource instanceof DirectoryResource) { - $this->assertNotContains('translations', $resource->getResource()); - } - - if ($resource instanceof FileExistenceResource) { - $this->assertNotContains('translations', $resource->getResource()); - } - } + $this->assertSame('Fixtures/translations', $options['cache_vary']['scanned_directories'][3]); } /** @@ -882,12 +938,19 @@ public function testTranslatorMultipleFallbacks() $this->assertEquals(['en', 'fr'], $calls[1][1][0]); } + public function testTranslatorCacheDirDisabled() + { + $container = $this->createContainerFromFile('translator_cache_dir_disabled'); + $options = $container->getDefinition('translator.default')->getArgument(4); + $this->assertNull($options['cache_dir']); + } + /** * @group legacy - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException */ public function testTemplatingRequiresAtLeastOneEngine() { + $this->expectException(InvalidConfigurationException::class); $container = $this->createContainer(); $loader = new FrameworkExtension(); $loader->load([['templating' => null]], $container); @@ -898,7 +961,7 @@ public function testValidation() $container = $this->createContainerFromFile('full'); $projectDir = $container->getParameter('kernel.project_dir'); - $ref = new \ReflectionClass('Symfony\Component\Form\Form'); + $ref = new \ReflectionClass(Form::class); $xmlMappings = [ \dirname($ref->getFileName()).'/Resources/config/validation.xml', strtr($projectDir.'/config/validator/foo.xml', '/', \DIRECTORY_SEPARATOR), @@ -913,9 +976,9 @@ public function testValidation() $this->assertEquals([new Reference('validator.validator_factory')], $calls[0][1]); $this->assertSame('setTranslator', $calls[1][0]); if (interface_exists(TranslatorInterface::class) && class_exists(LegacyTranslatorProxy::class)) { - $this->assertEquals([new Definition(LegacyTranslatorProxy::class, [new Reference('translator')])], $calls[1][1]); + $this->assertEquals([new Definition(LegacyTranslatorProxy::class, [new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)])], $calls[1][1]); } else { - $this->assertEquals([new Reference('translator')], $calls[1][1]); + $this->assertEquals([new Reference('translator', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)], $calls[1][1]); } $this->assertSame('setTranslationDomain', $calls[2][0]); $this->assertSame(['%validator.translation_domain%'], $calls[2][1]); @@ -927,15 +990,15 @@ public function testValidation() } $this->assertSame('addMethodMapping', $calls[++$i][0]); $this->assertSame(['loadValidatorMetadata'], $calls[$i][1]); - $this->assertSame('setMetadataCache', $calls[++$i][0]); - $this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[$i][1]); + $this->assertSame('setMappingCache', $calls[++$i][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[$i][1]); } public function testValidationService() { $container = $this->createContainerFromFile('validation_annotations', ['kernel.charset' => 'UTF-8'], false); - $this->assertInstanceOf('Symfony\Component\Validator\Validator\ValidatorInterface', $container->get('validator')); + $this->assertInstanceOf(ValidatorInterface::class, $container->get('validator')); } public function testAnnotations() @@ -944,13 +1007,17 @@ public function testAnnotations() $container->addCompilerPass(new TestAnnotationsPass()); $container->compile(); - $this->assertEquals($container->getParameter('kernel.cache_dir').'/annotations', $container->getDefinition('annotations.filesystem_cache')->getArgument(0)); - $this->assertSame('annotations.filesystem_cache', (string) $container->getDefinition('annotation_reader')->getArgument(1)); + $this->assertEquals($container->getParameter('kernel.cache_dir').'/annotations', $container->getDefinition('annotations.filesystem_cache_adapter')->getArgument(2)); + if (class_exists(PsrCachedReader::class)) { + $this->assertSame('annotations.filesystem_cache_adapter', (string) $container->getDefinition('annotation_reader')->getArgument(1)); + } else { + $this->assertSame('annotations.filesystem_cache', (string) $container->getDefinition('annotation_reader')->getArgument(1)); + } } public function testFileLinkFormat() { - if (ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) { + if (\ini_get('xdebug.file_link_format') || get_cfg_var('xdebug.file_link_format')) { $this->markTestSkipped('A custom file_link_format is defined.'); } @@ -970,8 +1037,8 @@ public function testValidationAnnotations() $this->assertEquals([new Reference('annotation_reader')], $calls[4][1]); $this->assertSame('addMethodMapping', $calls[5][0]); $this->assertSame(['loadValidatorMetadata'], $calls[5][1]); - $this->assertSame('setMetadataCache', $calls[6][0]); - $this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[6][1]); + $this->assertSame('setMappingCache', $calls[6][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[6][1]); // no cache this time } @@ -992,8 +1059,8 @@ public function testValidationPaths() $this->assertSame('enableAnnotationMapping', $calls[5][0]); $this->assertSame('addMethodMapping', $calls[6][0]); $this->assertSame(['loadValidatorMetadata'], $calls[6][1]); - $this->assertSame('setMetadataCache', $calls[7][0]); - $this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[7][1]); + $this->assertSame('setMappingCache', $calls[7][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[7][1]); $xmlMappings = $calls[3][1][0]; $this->assertCount(3, $xmlMappings); @@ -1052,19 +1119,19 @@ public function testValidationNoStaticMethod() if ($annotations) { $this->assertSame('enableAnnotationMapping', $calls[++$i][0]); } - $this->assertSame('setMetadataCache', $calls[++$i][0]); - $this->assertEquals([new Reference('validator.mapping.cache.symfony')], $calls[$i][1]); + $this->assertSame('setMappingCache', $calls[++$i][0]); + $this->assertEquals([new Reference('validator.mapping.cache.adapter')], $calls[$i][1]); // no cache, no annotations, no static methods } /** * @group legacy * @expectedDeprecation The "framework.validation.strict_email" configuration key has been deprecated in Symfony 4.1. Use the "framework.validation.email_validation_mode" configuration key instead. - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "strict_email" and "email_validation_mode" cannot be used together. */ public function testCannotConfigureStrictEmailAndEmailValidationModeAtTheSameTime() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('"strict_email" and "email_validation_mode" cannot be used together.'); $this->createContainerFromFile('validation_strict_email_and_validation_mode'); } @@ -1115,9 +1182,9 @@ public function testValidationMapping() $this->assertSame('addYamlMappings', $calls[4][0]); $this->assertCount(3, $calls[4][1][0]); - $this->assertContains('foo.yml', $calls[4][1][0][0]); - $this->assertContains('validation.yml', $calls[4][1][0][1]); - $this->assertContains('validation.yaml', $calls[4][1][0][2]); + $this->assertStringContainsString('foo.yml', $calls[4][1][0][0]); + $this->assertStringContainsString('validation.yml', $calls[4][1][0][1]); + $this->assertStringContainsString('validation.yaml', $calls[4][1][0][2]); } public function testValidationAutoMapping() @@ -1178,7 +1245,6 @@ public function testSerializerEnabled() $this->assertCount(2, $argument); $this->assertEquals('Symfony\Component\Serializer\Mapping\Loader\AnnotationLoader', $argument[0]->getClass()); - $this->assertNull($container->getDefinition('serializer.mapping.class_metadata_factory')->getArgument(1)); $this->assertEquals(new Reference('serializer.name_converter.camel_case_to_snake_case'), $container->getDefinition('serializer.name_converter.metadata_aware')->getArgument(1)); $this->assertEquals(new Reference('property_info', ContainerBuilder::IGNORE_ON_INVALID_REFERENCE), $container->getDefinition('serializer.normalizer.object')->getArgument(3)); $this->assertArrayHasKey('circular_reference_handler', $container->getDefinition('serializer.normalizer.object')->getArgument(6)); @@ -1239,7 +1305,7 @@ public function testJsonSerializableNormalizerRegistered() $tag = $definition->getTag('serializer.normalizer'); $this->assertEquals(JsonSerializableNormalizer::class, $definition->getClass()); - $this->assertEquals(-900, $tag[0]['priority']); + $this->assertEquals(-950, $tag[0]['priority']); } public function testObjectNormalizerRegistered() @@ -1416,20 +1482,6 @@ public function testCacheDefaultRedisProvider() $this->assertSame($redisUrl, $url); } - public function testCacheDefaultRedisProviderWithEnvVar() - { - $container = $this->createContainerFromFile('cache_env_var'); - - $redisUrl = 'redis://paas.com'; - $providerId = '.cache_connection.'.ContainerBuilder::hash($redisUrl); - - $this->assertTrue($container->hasDefinition($providerId)); - - $url = $container->getDefinition($providerId)->getArgument(0); - - $this->assertSame($redisUrl, $url); - } - public function testCachePoolServices() { $container = $this->createContainerFromFile('cache', [], true, false); @@ -1447,21 +1499,37 @@ public function testCachePoolServices() $this->assertSame(ChainAdapter::class, $chain->getClass()); + $this->assertCount(2, $chain->getArguments()); + $this->assertCount(3, $chain->getArguments()[0]); + + $expectedSeed = $chain->getArgument(0)[1]->getArgument(0); $expected = [ [ (new ChildDefinition('cache.adapter.array')) ->replaceArgument(0, 12), (new ChildDefinition('cache.adapter.filesystem')) - ->replaceArgument(0, 'x5nX4TVTWn') + ->replaceArgument(0, $expectedSeed) ->replaceArgument(1, 12), (new ChildDefinition('cache.adapter.redis')) ->replaceArgument(0, new Reference('.cache_connection.kYdiLgf')) - ->replaceArgument(1, 'x5nX4TVTWn') + ->replaceArgument(1, $expectedSeed) ->replaceArgument(2, 12), ], 12, ]; $this->assertEquals($expected, $chain->getArguments()); + + // Test "tags: true" wrapping logic + $tagAwareDefinition = $container->getDefinition('cache.ccc'); + $this->assertSame(TagAwareAdapter::class, $tagAwareDefinition->getClass()); + $this->assertCachePoolServiceDefinitionIsCreated($container, (string) $tagAwareDefinition->getArgument(0), 'cache.adapter.array', 410); + + if (method_exists(TagAwareAdapter::class, 'setLogger')) { + $this->assertEquals([ + ['setLogger', [new Reference('logger', ContainerInterface::IGNORE_ON_INVALID_REFERENCE)]], + ], $tagAwareDefinition->getMethodCalls()); + $this->assertSame([['channel' => 'cache']], $tagAwareDefinition->getTag('monolog.logger')); + } } public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebug() @@ -1535,6 +1603,14 @@ public function testHttpClientDefaultOptions() $this->assertSame(ScopingHttpClient::class, $container->getDefinition('foo')->getClass()); } + public function testScopedHttpClientWithoutQueryOption() + { + $container = $this->createContainerFromFile('http_client_scoped_without_query_option'); + + $this->assertTrue($container->hasDefinition('foo'), 'should have the "foo" service.'); + $this->assertSame(ScopingHttpClient::class, $container->getDefinition('foo')->getClass()); + } + public function testHttpClientOverrideDefaultOptions() { $container = $this->createContainerFromFile('http_client_override_default_options'); @@ -1544,7 +1620,6 @@ public function testHttpClientOverrideDefaultOptions() $this->assertSame('http://example.com', $container->getDefinition('foo')->getArgument(1)); $expected = [ - 'base_uri' => 'http://example.com', 'headers' => [ 'bar' => 'baz', ], @@ -1554,6 +1629,21 @@ public function testHttpClientOverrideDefaultOptions() $this->assertSame($expected, $container->getDefinition('foo')->getArgument(2)); } + public function testHttpClientWithQueryParameterKey() + { + $container = $this->createContainerFromFile('http_client_xml_key'); + + $expected = [ + 'key' => 'foo', + ]; + $this->assertSame($expected, $container->getDefinition('foo')->getArgument(2)['query']); + + $expected = [ + 'host' => '127.0.0.1', + ]; + $this->assertSame($expected, $container->getDefinition('foo')->getArgument(2)['resolve']); + } + public function testHttpClientFullDefaultOptions() { $container = $this->createContainerFromFile('http_client_full_default_options'); @@ -1566,6 +1656,7 @@ public function testHttpClientFullDefaultOptions() $this->assertSame(['localhost' => '127.0.0.1'], $defaultOptions['resolve']); $this->assertSame('proxy.org', $defaultOptions['proxy']); $this->assertSame(3.5, $defaultOptions['timeout']); + $this->assertSame(10.1, $defaultOptions['max_duration']); $this->assertSame('127.0.0.1', $defaultOptions['bindto']); $this->assertTrue($defaultOptions['verify_peer']); $this->assertTrue($defaultOptions['verify_host']); @@ -1581,22 +1672,54 @@ public function testHttpClientFullDefaultOptions() ], $defaultOptions['peer_fingerprint']); } - public function testMailer(): void + public function provideMailer(): array { - $container = $this->createContainerFromFile('mailer'); + return [ + ['mailer_with_dsn', ['main' => 'smtp://example.com']], + ['mailer_with_transports', [ + 'transport1' => 'smtp://example1.com', + 'transport2' => 'smtp://example2.com', + ]], + ]; + } + + /** + * @dataProvider provideMailer + */ + public function testMailer(string $configFile, array $expectedTransports) + { + $container = $this->createContainerFromFile($configFile); $this->assertTrue($container->hasAlias('mailer')); + $this->assertTrue($container->hasDefinition('mailer.transports')); + $this->assertSame($expectedTransports, $container->getDefinition('mailer.transports')->getArgument(0)); $this->assertTrue($container->hasDefinition('mailer.default_transport')); - $this->assertSame('smtp://example.com', $container->getDefinition('mailer.default_transport')->getArgument(0)); + $this->assertSame(current($expectedTransports), $container->getDefinition('mailer.default_transport')->getArgument(0)); $this->assertTrue($container->hasDefinition('mailer.envelope_listener')); $l = $container->getDefinition('mailer.envelope_listener'); $this->assertSame('sender@example.org', $l->getArgument(0)); $this->assertSame(['redirected@example.org', 'redirected1@example.org'], $l->getArgument(1)); } + public function testRegisterParameterCollectingBehaviorDescribingTags() + { + $container = $this->createContainerFromFile('default_config'); + + $this->assertTrue($container->hasParameter('container.behavior_describing_tags')); + $this->assertEquals([ + 'annotations.cached_reader', + 'container.do_not_inline', + 'container.service_locator', + 'container.service_subscriber', + 'kernel.event_subscriber', + 'kernel.locale_aware', + 'kernel.reset', + ], $container->getParameter('container.behavior_describing_tags')); + } + protected function createContainer(array $data = []) { - return new ContainerBuilder(new ParameterBag(array_merge([ + return new ContainerBuilder(new EnvPlaceholderParameterBag(array_merge([ 'kernel.bundles' => ['FrameworkBundle' => 'Symfony\\Bundle\\FrameworkBundle\\FrameworkBundle'], 'kernel.bundles_metadata' => ['FrameworkBundle' => ['namespace' => 'Symfony\\Bundle\\FrameworkBundle', 'path' => __DIR__.'/../..']], 'kernel.cache_dir' => __DIR__, @@ -1614,7 +1737,7 @@ protected function createContainer(array $data = []) protected function createContainerFromFile($file, $data = [], $resetCompilerPasses = true, $compile = true) { - $cacheKey = md5(\get_class($this).$file.serialize($data)); + $cacheKey = md5(static::class.$file.serialize($data)); if ($compile && isset(self::$containerCache[$cacheKey])) { return self::$containerCache[$cacheKey]; } @@ -1625,9 +1748,10 @@ protected function createContainerFromFile($file, $data = [], $resetCompilerPass if ($resetCompilerPasses) { $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); } $container->getCompilerPassConfig()->setBeforeOptimizationPasses([new LoggerPass()]); - $container->getCompilerPassConfig()->setBeforeRemovingPasses([new AddConstraintValidatorsPass(), new TranslatorPass('translator.default', 'translation.reader')]); + $container->getCompilerPassConfig()->setBeforeRemovingPasses([new AddConstraintValidatorsPass(), new TranslatorPass()]); $container->getCompilerPassConfig()->setAfterRemovingPasses([new AddAnnotationsCachedReaderPass()]); if (!$compile) { @@ -1647,6 +1771,7 @@ protected function createContainerFromClosure($closure, $data = []) $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); return $container; @@ -1716,6 +1841,9 @@ private function assertCachePoolServiceDefinitionIsCreated(ContainerBuilder $con case 'cache.adapter.redis': $this->assertSame(RedisAdapter::class, $parentDefinition->getClass()); break; + case 'cache.adapter.array': + $this->assertSame(ArrayAdapter::class, $parentDefinition->getClass()); + break; default: $this->fail('Unresolved adapter: '.$adapter); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php index a67a35844769f..1c69d35e4e813 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/PhpFrameworkExtensionTest.php @@ -14,6 +14,7 @@ use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\PhpFileLoader; +use Symfony\Component\Workflow\Exception\InvalidDefinitionException; class PhpFrameworkExtensionTest extends FrameworkExtensionTest { @@ -23,11 +24,9 @@ protected function loadFromFile(ContainerBuilder $container, $file) $loader->load($file.'.php'); } - /** - * @expectedException \LogicException - */ public function testAssetsCannotHavePathAndUrl() { + $this->expectException(\LogicException::class); $this->createContainerFromClosure(function ($container) { $container->loadFromExtension('framework', [ 'assets' => [ @@ -38,11 +37,9 @@ public function testAssetsCannotHavePathAndUrl() }); } - /** - * @expectedException \LogicException - */ public function testAssetPackageCannotHavePathAndUrl() { + $this->expectException(\LogicException::class); $this->createContainerFromClosure(function ($container) { $container->loadFromExtension('framework', [ 'assets' => [ @@ -57,12 +54,10 @@ public function testAssetPackageCannotHavePathAndUrl() }); } - /** - * @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException - * @expectedExceptionMessage A transition from a place/state must have an unique name. Multiple transitions named "a_to_b" from place/state "a" where found on StateMachine "article". - */ public function testWorkflowValidationStateMachine() { + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('A transition from a place/state must have an unique name. Multiple transitions named "a_to_b" from place/state "a" were found on StateMachine "article".'); $this->createContainerFromClosure(function ($container) { $container->loadFromExtension('framework', [ 'workflows' => [ @@ -159,12 +154,12 @@ public function testWorkflowValidationMultipleState() } /** - * @expectedException \Symfony\Component\Workflow\Exception\InvalidDefinitionException - * @expectedExceptionMessage The marking store of workflow "article" can not store many places. But the transition "a_to_b" has too many output (2). Only one is accepted. * @group legacy */ public function testWorkflowValidationSingleState() { + $this->expectException(InvalidDefinitionException::class); + $this->expectExceptionMessage('The marking store of workflow "article" can not store many places. But the transition "a_to_b" has too many output (2). Only one is accepted.'); $this->createContainerFromClosure(function ($container) { $container->loadFromExtension('framework', [ 'workflows' => [ diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/BaseBundle/Resources/foo.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/translations/security.en.yaml similarity index 100% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/BaseBundle/Resources/foo.txt rename to src/Symfony/Bundle/FrameworkBundle/Tests/DependencyInjection/translations/security.en.yaml diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php index 362f00e95c29b..2aa519acfb2b1 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/EventListener/ResolveControllerNameSubscriberTest.php @@ -26,12 +26,12 @@ class ResolveControllerNameSubscriberTest extends TestCase { public function testReplacesControllerAttribute() { - $parser = $this->getMockBuilder(ControllerNameParser::class)->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(ControllerNameParser::class); $parser->expects($this->any()) ->method('parse') ->with('AppBundle:Starting:format') ->willReturn('App\\Final\\Format::methodName'); - $httpKernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $httpKernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_controller', 'AppBundle:Starting:format'); @@ -50,10 +50,10 @@ public function testReplacesControllerAttribute() */ public function testSkipsOtherControllerFormats($controller) { - $parser = $this->getMockBuilder(ControllerNameParser::class)->disableOriginalConstructor()->getMock(); + $parser = $this->createMock(ControllerNameParser::class); $parser->expects($this->never()) ->method('parse'); - $httpKernel = $this->getMockBuilder(HttpKernelInterface::class)->getMock(); + $httpKernel = $this->createMock(HttpKernelInterface::class); $request = new Request(); $request->attributes->set('_controller', $controller); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/ClassAliasExampleClass.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/ClassAliasExampleClass.php new file mode 100644 index 0000000000000..8303dc6ea1ec3 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/ClassAliasExampleClass.php @@ -0,0 +1,14 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.json new file mode 100644 index 0000000000000..a9d9697847fdb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.json @@ -0,0 +1,109 @@ +{ + "definitions": { + "definition_3": { + "class": "Full\\Qualified\\Class3", + "public": true, + "synthetic": true, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "attr3": "val3", + "priority": 40 + } + }, + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "attr2": "val2", + "priority": 0 + } + } + ] + }, + "definition_1": { + "class": "Full\\Qualified\\Class1", + "public": true, + "synthetic": true, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "file": "\/path\/to\/file", + "factory_service": "factory.service", + "factory_method": "get", + "calls": [ + "setMailer" + ], + "tags": [ + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "priority": 30 + } + }, + { + "name": "tag1", + "parameters": { + "attr2": "val2" + } + }, + { + "name": "tag2", + "parameters": [] + } + ] + }, + "definition_4": { + "class": "Full\\Qualified\\Class4", + "public": true, + "synthetic": true, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "priority": 0 + } + } + ] + }, + "definition_2": { + "class": "Full\\Qualified\\Class2", + "public": true, + "synthetic": true, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "file": "\/path\/to\/file", + "tags": [ + { + "name": "tag1", + "parameters": { + "attr1": "val1", + "attr2": "val2", + "priority": -20 + } + } + ] + } + }, + "aliases": [], + "services": [] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.md new file mode 100644 index 0000000000000..8c0fef6aa3bea --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.md @@ -0,0 +1,75 @@ +Services with tag `tag1` +======================== + +Definitions +----------- + +### definition_3 + +- Class: `Full\Qualified\Class3` +- Public: yes +- Synthetic: yes +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- File: `/path/to/file` +- Tag: `tag1` + - Attr3: val3 + - Priority: 40 +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 + - Priority: 0 + +### definition_1 + +- Class: `Full\Qualified\Class1` +- Public: yes +- Synthetic: yes +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- File: `/path/to/file` +- Factory Service: `factory.service` +- Factory Method: `get` +- Call: `setMailer` +- Tag: `tag1` + - Attr1: val1 + - Priority: 30 +- Tag: `tag1` + - Attr2: val2 +- Tag: `tag2` + +### definition_4 + +- Class: `Full\Qualified\Class4` +- Public: yes +- Synthetic: yes +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- File: `/path/to/file` +- Tag: `tag1` + - Priority: 0 + +### definition_2 + +- Class: `Full\Qualified\Class2` +- Public: yes +- Synthetic: yes +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- File: `/path/to/file` +- Tag: `tag1` + - Attr1: val1 + - Attr2: val2 + - Priority: -20 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt new file mode 100644 index 0000000000000..7884a05c2a690 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.txt @@ -0,0 +1,15 @@ + +Symfony Container Services Tagged with "tag1" Tag +================================================= + + -------------- ------- ------- ---------- ------- ----------------------- +  Service ID   attr1   attr2   priority   attr3   Class name  + -------------- ------- ------- ---------- ------- ----------------------- + definition_3 40 val3 Full\Qualified\Class3 + " val1 val2 0 + definition_1 val1 30 Full\Qualified\Class1 + " val2 + definition_4 0 Full\Qualified\Class4 + definition_2 val1 val2 -20 Full\Qualified\Class2 + -------------- ------- ------- ---------- ------- ----------------------- + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.xml new file mode 100644 index 0000000000000..2e00c99955257 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/builder_priority_tag.xml @@ -0,0 +1,48 @@ + + + + + + val3 + 40 + + + val1 + val2 + 0 + + + + + + + + + + + val1 + 30 + + + val2 + + + + + + + + 0 + + + + + + + val1 + val2 + -20 + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt index 2d5b03794ea80..0ceb807a45c2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_2.txt @@ -3,9 +3,9 @@ ----------------- --------------------------------- Service ID - Class Full\Qualified\Class2 - Tags tag1 (attr1: val1, attr2: val2)  - tag1 (attr3: val3)  - tag2 + Tags tag1 (attr1: val1, attr2: val2) + tag1 (attr3: val3) + tag2 Calls setMailer Public no Synthetic yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.json new file mode 100644 index 0000000000000..4bf56746493f8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.json @@ -0,0 +1,14 @@ +{ + "class": "Full\\Qualified\\Class3", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "file": "\/path\/to\/file", + "factory_service": "inline factory service (Full\\Qualified\\FactoryClass)", + "factory_method": "get", + "tags": [] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.md new file mode 100644 index 0000000000000..68f51634db99f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.md @@ -0,0 +1,11 @@ +- Class: `Full\Qualified\Class3` +- Public: no +- Synthetic: no +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- File: `/path/to/file` +- Factory Service: inline factory service (`Full\Qualified\FactoryClass`) +- Factory Method: `get` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.txt new file mode 100644 index 0000000000000..35ddaf3e452a8 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.txt @@ -0,0 +1,18 @@ + ----------------- ------------------------------------------------------ +  Option   Value  + ----------------- ------------------------------------------------------ + Service ID - + Class Full\Qualified\Class3 + Tags - + Public no + Synthetic no + Lazy no + Shared yes + Abstract no + Autowired no + Autoconfigured no + Required File /path/to/file + Factory Service inline factory service (Full\Qualified\FactoryClass) + Factory Method get + ----------------- ------------------------------------------------------ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.xml new file mode 100644 index 0000000000000..e81c77014253f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_3.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt index 2e9dc9771c09d..a3caa93c9dcf9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_1.txt @@ -13,12 +13,12 @@ Autoconfigured no Factory Class Full\Qualified\FactoryClass Factory Method get - Arguments Service(.definition_2)  - %parameter%  - Inlined Service  - Array (3 element(s))  - Iterator (2 element(s))  - - Service(definition_1)  - - Service(.definition_2) + Arguments Service(.definition_2) + %parameter% + Inlined Service + Array (3 element(s)) + Iterator (2 element(s)) + - Service(definition_1) + - Service(.definition_2) ---------------- ----------------------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt index 2d5b03794ea80..0ceb807a45c2f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_2.txt @@ -3,9 +3,9 @@ ----------------- --------------------------------- Service ID - Class Full\Qualified\Class2 - Tags tag1 (attr1: val1, attr2: val2)  - tag1 (attr3: val3)  - tag2 + Tags tag1 (attr1: val1, attr2: val2) + tag1 (attr3: val3) + tag2 Calls setMailer Public no Synthetic yes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.json new file mode 100644 index 0000000000000..94c2fda5402fc --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.json @@ -0,0 +1,15 @@ +{ + "class": "Full\\Qualified\\Class3", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "arguments": [], + "file": "\/path\/to\/file", + "factory_service": "inline factory service (Full\\Qualified\\FactoryClass)", + "factory_method": "get", + "tags": [] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.md new file mode 100644 index 0000000000000..2ce1f264dfc6c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.md @@ -0,0 +1,12 @@ +- Class: `Full\Qualified\Class3` +- Public: no +- Synthetic: no +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- Arguments: no +- File: `/path/to/file` +- Factory Service: inline factory service (`Full\Qualified\FactoryClass`) +- Factory Method: `get` diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.txt new file mode 100644 index 0000000000000..6e400de44e8ff --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.txt @@ -0,0 +1,18 @@ + ----------------- ------------------------------------------------------ +  Option   Value  + ----------------- ------------------------------------------------------ + Service ID - + Class Full\Qualified\Class3 + Tags - + Public no + Synthetic no + Lazy no + Shared yes + Abstract no + Autowired no + Autoconfigured no + Required File /path/to/file + Factory Service inline factory service (Full\Qualified\FactoryClass) + Factory Method get + ----------------- ------------------------------------------------------ + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.xml new file mode 100644 index 0000000000000..e81c77014253f --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_3.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.json new file mode 100644 index 0000000000000..d5580ee3334a4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.json @@ -0,0 +1,15 @@ +{ + "class": "definition_with_enum", + "public": false, + "synthetic": false, + "lazy": false, + "shared": true, + "abstract": false, + "autowire": false, + "autoconfigure": false, + "arguments": [ + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\FooUnitEnum::FOO" + ], + "file": null, + "tags": [] +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.md new file mode 100644 index 0000000000000..78ef17f14b1fa --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.md @@ -0,0 +1,9 @@ +- Class: `definition_with_enum` +- Public: no +- Synthetic: no +- Lazy: no +- Shared: yes +- Abstract: no +- Autowired: no +- Autoconfigured: no +- Arguments: yes \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.txt new file mode 100644 index 0000000000000..9556f9b832f4e --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.txt @@ -0,0 +1,16 @@ + ---------------- ---------------------------------------------------------------- +  Option   Value  + ---------------- ---------------------------------------------------------------- + Service ID - + Class definition_with_enum + Tags - + Public no + Synthetic no + Lazy no + Shared yes + Abstract no + Autowired no + Autoconfigured no + Arguments Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum::FOO + ---------------- ---------------------------------------------------------------- + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.xml new file mode 100644 index 0000000000000..cb58a6d935e97 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/definition_arguments_with_enum.xml @@ -0,0 +1,4 @@ + + + Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum::FOO + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json index 4b68f0cefc0e4..dc9957f7141e5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.json @@ -1,7 +1,7 @@ [ { "type": "function", - "name": "global_function", + "name": "var_dump", "priority": 255 }, { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md index 98b81ecdce422..826ab219ed1fa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.md @@ -3,7 +3,7 @@ ## Listener 1 - Type: `function` -- Name: `global_function` +- Name: `var_dump` - Priority: `255` ## Listener 2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt index f7a3cb0bd90ca..0f0879f421b05 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.txt @@ -2,10 +2,10 @@ Registered Listeners for "event1" Event ======================================= - ------- ------------------- ---------- -  Order   Callable   Priority  - ------- ------------------- ---------- - #1 global_function() 255 - #2 Closure() -1 - ------- ------------------- ---------- + ------- ------------ ---------- +  Order   Callable   Priority  + ------- ------------ ---------- + #1 var_dump() 255 + #2 Closure() -1 + ------- ------------ ---------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml index bc03189af7b80..3d387b44bbf27 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_event1.xml @@ -1,5 +1,5 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json index 30772d9a4a212..f79f79f99e21d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.json @@ -2,7 +2,7 @@ "event1": [ { "type": "function", - "name": "global_function", + "name": "var_dump", "priority": 255 }, { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md index eb809789d5f17..ba407bef0c09d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.md @@ -5,7 +5,7 @@ ### Listener 1 - Type: `function` -- Name: `global_function` +- Name: `var_dump` - Priority: `255` ### Listener 2 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt index 475ad24cfda20..35c68295b8bfa 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.txt @@ -5,12 +5,12 @@ "event1" event -------------- - ------- ------------------- ---------- -  Order   Callable   Priority  - ------- ------------------- ---------- - #1 global_function() 255 - #2 Closure() -1 - ------- ------------------- ---------- + ------- ------------ ---------- +  Order   Callable   Priority  + ------- ------------ ---------- + #1 var_dump() 255 + #2 Closure() -1 + ------- ------------ ---------- "event2" event -------------- diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml index d7443f9743666..57a4b3a5cf6cd 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/event_dispatcher_1_events.xml @@ -1,7 +1,7 @@ - + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.json b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.json new file mode 100644 index 0000000000000..34d9e8f773d15 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.json @@ -0,0 +1,17 @@ +{ + "array_of_enums": [ + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Hearts", + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Diamonds", + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Clubs", + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Spades" + ], + "backed_enum": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Hearts", + "map": { + "mixed": [ + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::Hearts", + "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\FooUnitEnum::BAR" + ], + "single": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\FooUnitEnum::BAR" + }, + "unit_enum": "Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\FooUnitEnum::BAR" +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.md b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.md new file mode 100644 index 0000000000000..e129a6c07ff0d --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.md @@ -0,0 +1,7 @@ +Container parameters +==================== + +- `array_of_enums`: `["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::H...` +- `backed_enum`: `Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Suit::Hearts` +- `map`: `{"mixed":["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures...` +- `unit_enum`: `Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum::BAR` \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.txt new file mode 100644 index 0000000000000..42c6938150d7c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.txt @@ -0,0 +1,11 @@ +Symfony Container Parameters +============================ + + ---------------- ----------------------------------------------------------------- +  Parameter   Value  + ---------------- ----------------------------------------------------------------- + array_of_enums ["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::H... + backed_enum Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Suit::Hearts + map {"mixed":["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures... + unit_enum Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum::BAR + ---------------- ----------------------------------------------------------------- \ No newline at end of file diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.xml b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.xml new file mode 100644 index 0000000000000..8fd512337916a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/parameters_enums.xml @@ -0,0 +1,7 @@ + + + ["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures\\Suit::H... + Symfony\Bundle\FrameworkBundle\Tests\Fixtures\Suit::Hearts + {"mixed":["Symfony\\Bundle\\FrameworkBundle\\Tests\\Fixtures... + Symfony\Bundle\FrameworkBundle\Tests\Fixtures\FooUnitEnum::BAR + diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt index 25074dfd18b2c..9814273b7a221 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1.txt @@ -11,7 +11,7 @@ | Requirements | name: [a-z]+ | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | name: Joseph | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | +--------------+-------------------------------------------------------------------+ diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt index 5853dd013d3a3..533409d402add 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2.txt @@ -11,8 +11,8 @@ | Requirements | NO CUSTOM | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | | Defaults | NONE | -| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | -| | opt1: val1 | -| | opt2: val2 | +| Options | compiler_class: Symfony\Component\Routing\RouteCompiler | +| | opt1: val1 | +| | opt2: val2 | | Condition | context.getMethod() in ['GET', 'HEAD', 'POST'] | +--------------+-------------------------------------------------------------------+ diff --git a/src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/FooUnitEnum.php similarity index 67% rename from src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/FooUnitEnum.php index 4020ced161fc1..02366bcf59c40 100644 --- a/src/Symfony/Component/ErrorRenderer/Exception/ErrorRendererNotFoundException.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/FooUnitEnum.php @@ -9,8 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\ErrorRenderer\Exception; +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures; -class ErrorRendererNotFoundException extends \RuntimeException +enum FooUnitEnum { + case BAR; + case FOO; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessage.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessage.php new file mode 100644 index 0000000000000..7f25eb4e5b9ed --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessage.php @@ -0,0 +1,18 @@ +message = $message; + } + + public function getMessage(): string + { + return $this->message; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessageInterface.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessageInterface.php new file mode 100644 index 0000000000000..a7243368afd63 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Messenger/DummyMessageInterface.php @@ -0,0 +1,7 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Fixtures; + +enum Suit: string +{ + case Hearts = 'H'; + case Diamonds = 'D'; + case Clubs = 'C'; + case Spades = 'S'; +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php new file mode 100644 index 0000000000000..4de75ac5b3f35 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TokenInterface.php @@ -0,0 +1,11 @@ +headers->get('Location')); } - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { static::deleteTmpDir(); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { static::deleteTmpDir(); } @@ -42,14 +43,14 @@ protected static function deleteTmpDir() $fs->remove($dir); } - protected static function getKernelClass() + protected static function getKernelClass(): string { require_once __DIR__.'/app/AppKernel.php'; return 'Symfony\Bundle\FrameworkBundle\Tests\Functional\app\AppKernel'; } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { $class = self::getKernelClass(); @@ -60,14 +61,14 @@ protected static function createKernel(array $options = []) return new $class( static::getVarDir(), $options['test_case'], - isset($options['root_config']) ? $options['root_config'] : 'config.yml', - isset($options['environment']) ? $options['environment'] : strtolower(static::getVarDir().$options['test_case']), - isset($options['debug']) ? $options['debug'] : false + $options['root_config'] ?? 'config.yml', + $options['environment'] ?? strtolower(static::getVarDir().$options['test_case']), + $options['debug'] ?? false ); } protected static function getVarDir() { - return 'FB'.substr(strrchr(\get_called_class(), '\\'), 1); + return 'FB'.substr(strrchr(static::class, '\\'), 1); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php index 51a3e7ee54247..c9ede7a9cf646 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AnnotatedControllerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class AnnotatedControllerTest extends WebTestCase +class AnnotatedControllerTest extends AbstractWebTestCase { /** * @dataProvider getRoutes diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php index e4338e3746c11..fdeaf98fb0293 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/AutowiringTypesTest.php @@ -13,13 +13,15 @@ use Doctrine\Common\Annotations\AnnotationReader; use Doctrine\Common\Annotations\CachedReader; +use Doctrine\Common\Annotations\PsrCachedReader; use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface as FrameworkBundleEngineInterface; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Templating\EngineInterface as ComponentEngineInterface; -class AutowiringTypesTest extends WebTestCase +class AutowiringTypesTest extends AbstractWebTestCase { public function testAnnotationReaderAutowiring() { @@ -34,7 +36,11 @@ public function testCachedAnnotationReaderAutowiring() static::bootKernel(); $annotationReader = static::$container->get('test.autowiring_types.autowired_services')->getAnnotationReader(); - $this->assertInstanceOf(CachedReader::class, $annotationReader); + if (class_exists(PsrCachedReader::class)) { + $this->assertInstanceOf(PsrCachedReader::class, $annotationReader); + } else { + $this->assertInstanceOf(CachedReader::class, $annotationReader); + } } /** @@ -70,7 +76,7 @@ public function testCacheAutowiring() $this->assertInstanceOf(FilesystemAdapter::class, $autowiredServices->getCachePool()); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return parent::createKernel(['test_case' => 'AutowiringTypes'] + $options); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.php new file mode 100644 index 0000000000000..2a53127eaa41a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DefaultConfigTestBundle.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\DefaultConfigTestBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class DefaultConfigTestBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000000000..ddd1589495139 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/Configuration.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\DefaultConfigTestBundle\DependencyInjection; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; + +class Configuration implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('default_config_test'); + + $treeBuilder->getRootNode() + ->children() + ->scalarNode('foo')->defaultValue('%default_config_test_foo%')->end() + ->scalarNode('baz')->defaultValue('%env(BAZ)%')->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.php new file mode 100644 index 0000000000000..400384f616f29 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/DefaultConfigTestBundle/DependencyInjection/DefaultConfigTestExtension.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\DefaultConfigTestBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; + +class DefaultConfigTestExtension extends Extension +{ + public function load(array $configs, ContainerBuilder $container) + { + $configuration = new Configuration(); + $config = $this->processConfiguration($configuration, $configs); + + $container->setParameter('default_config_test', $config['foo']); + $container->setParameter('default_config_test', $config['baz']); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.php new file mode 100644 index 0000000000000..a3416b4686ad1 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/DependencyInjection/ExtensionWithoutConfigTestExtension.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle\DependencyInjection; + +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; + +class ExtensionWithoutConfigTestExtension implements ExtensionInterface +{ + public function load(array $configs, ContainerBuilder $container) + { + } + + public function getNamespace(): string + { + return ''; + } + + public function getXsdValidationBasePath() + { + return false; + } + + public function getAlias(): string + { + return 'extension_without_config_test'; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/ExtensionWithoutConfigTestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/ExtensionWithoutConfigTestBundle.php new file mode 100644 index 0000000000000..71fae639a1db6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ExtensionWithoutConfigTestBundle/ExtensionWithoutConfigTestBundle.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class ExtensionWithoutConfigTestBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Entity/LegacyPerson.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Entity/LegacyPerson.php new file mode 100644 index 0000000000000..8135e95e89c02 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Entity/LegacyPerson.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity; + +class LegacyPerson +{ + public $name; + public $age; + + public function __construct(string $name, string $age) + { + $this->name = $name; + $this->age = $age; + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionAbsentBundle/ExtensionAbsentBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/LegacyBundle.php similarity index 70% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionAbsentBundle/ExtensionAbsentBundle.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/LegacyBundle.php index c8bfd36e662f5..e38e38824b324 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionAbsentBundle/ExtensionAbsentBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/LegacyBundle.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionAbsentBundle; +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; -class ExtensionAbsentBundle extends Bundle +class LegacyBundle extends Bundle { } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/serialization.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/serialization.yaml new file mode 100644 index 0000000000000..c878793ea9a2a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/serialization.yaml @@ -0,0 +1,4 @@ +Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson: + attributes: + name: + serialized_name: 'full_name' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/validation.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/validation.yaml new file mode 100644 index 0000000000000..c59b8cb168bb2 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/config/validation.yaml @@ -0,0 +1,5 @@ +Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson: + properties: + age: + - GreaterThan: + value: 18 diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/BaseBundle/Resources/hide.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/public/legacy.css similarity index 100% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/BaseBundle/Resources/hide.txt rename to src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/public/legacy.css diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/translations/legacy.en.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/translations/legacy.en.yaml new file mode 100644 index 0000000000000..1860a9d6340b6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/translations/legacy.en.yaml @@ -0,0 +1 @@ +ok_label: OK diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/views/index.html.twig b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/views/index.html.twig new file mode 100644 index 0000000000000..d86bac9de59ab --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/LegacyBundle/Resources/views/index.html.twig @@ -0,0 +1 @@ +OK diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/serialization.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/serialization.yaml new file mode 100644 index 0000000000000..d83e457f0a2ec --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/serialization.yaml @@ -0,0 +1,4 @@ +Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson: + attributes: + name: + serialized_name: 'full_name' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/validation.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/validation.yaml new file mode 100644 index 0000000000000..f3044c3b19edb --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/config/validation.yaml @@ -0,0 +1,5 @@ +Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson: + properties: + age: + - GreaterThan: + value: 18 diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle1Bundle/Resources/foo.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/public/modern.css similarity index 100% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle1Bundle/Resources/foo.txt rename to src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/public/modern.css diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/Entity/ModernPerson.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/Entity/ModernPerson.php new file mode 100644 index 0000000000000..6c22925d65eb0 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/Entity/ModernPerson.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity; + +class ModernPerson +{ + public $name; + public $age; + + public function __construct(string $name, string $age) + { + $this->name = $name; + $this->age = $age; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/FabpotFooBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/ModernBundle.php similarity index 54% rename from src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/FabpotFooBundle.php rename to src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/ModernBundle.php index 17894ba34146b..cc29f998ee964 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/TestBundle/Fabpot/FooBundle/FabpotFooBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/src/ModernBundle.php @@ -9,22 +9,14 @@ * file that was distributed with this source code. */ -namespace TestBundle\Fabpot\FooBundle; +namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src; use Symfony\Component\HttpKernel\Bundle\Bundle; -/** - * Bundle. - * - * @author Fabien Potencier - */ -class FabpotFooBundle extends Bundle +class ModernBundle extends Bundle { - /** - * {@inheritdoc} - */ - public function getParent() + public function getPath(): string { - return 'SensioFooBundle'; + return \dirname(__DIR__); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/templates/index.html.twig b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/templates/index.html.twig new file mode 100644 index 0000000000000..d86bac9de59ab --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/templates/index.html.twig @@ -0,0 +1 @@ +OK diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/translations/modern.en.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/translations/modern.en.yaml new file mode 100644 index 0000000000000..1860a9d6340b6 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/ModernBundle/translations/modern.en.yaml @@ -0,0 +1 @@ +ok_label: OK diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php index 7fc0cdd7b55af..07bb8ed1ce3d4 100644 --- a/src/Symfony/Bundle/Framew 10000 orkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/AutowiringTypes/TemplatingServices.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\AutowiringTypes; use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface as FrameworkBundleEngineInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/EmailController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/EmailController.php new file mode 100644 index 0000000000000..1a871f79be907 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/EmailController.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Address; +use Symfony\Component\Mime\Email; + +class EmailController +{ + public function indexAction(MailerInterface $mailer) + { + $mailer->send((new Email())->to('fabien@symfony.com')->from('fabien@symfony.com')->subject('Foo') + ->addReplyTo('me@symfony.com') + ->addCc('cc@symfony.com') + ->text('Bar!') + ->html('

Foo

') + ->attach(file_get_contents(__FILE__), 'foobar.php') + ); + + $mailer->send((new Email())->to('fabien@symfony.com', 'thomas@symfony.com')->from('fabien@symfony.com')->subject('Foo') + ->addReplyTo(new Address('me@symfony.com', 'Fabien Potencier')) + ->addCc('cc@symfony.com') + ->text('Bar!') + ->html('

Foo

') + ->attach(file_get_contents(__FILE__), 'foobar.php') + ); + + return new Response(); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.php new file mode 100644 index 0000000000000..f616aac401842 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/InjectedFlashbagSessionController.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\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller; + +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Session\Flash\FlashBagInterface; +use Symfony\Component\Routing\RouterInterface; + +class InjectedFlashbagSessionController +{ + /** + * @var FlashBagInterface + */ + private $flashBag; + + /** + * @var RouterInterface + */ + private $router; + + public function __construct( + FlashBagInterface $flashBag, + RouterInterface $router + ) { + $this->flashBag = $flashBag; + $this->router = $router; + } + + public function setFlashAction(Request $request, $message) + { + $this->flashBag->add('notice', $message); + + return new RedirectResponse($this->router->generate('session_showflash')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php index 0d9464d7dfab4..168cd2d45a52a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Controller/SessionController.php @@ -71,7 +71,7 @@ public function showFlashAction(Request $request) $session = $request->getSession(); if ($session->getFlashBag()->has('notice')) { - list($output) = $session->getFlashBag()->get('notice'); + [$output] = $session->getFlashBag()->get('notice'); } else { $output = 'No flash was set.'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php index 9022bc24c26de..fb70137ff7b22 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/Configuration.php @@ -23,7 +23,7 @@ public function __construct($customConfig = null) $this->customConfig = $customConfig; } - public function getConfigTreeBuilder() + public function getConfigTreeBuilder(): TreeBuilder { $treeBuilder = new TreeBuilder('test'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php index 59670fdd19a24..2d88510520531 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TestExtension.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection; +use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; @@ -26,7 +27,7 @@ class TestExtension extends Extension implements PrependExtensionInterface public function load(array $configs, ContainerBuilder $container) { $configuration = $this->getConfiguration($configs, $container); - $config = $this->processConfiguration($configuration, $configs); + $this->processConfiguration($configuration, $configs); $container->setAlias('test.annotation_reader', new Alias('annotation_reader', true)); } @@ -42,7 +43,7 @@ public function prepend(ContainerBuilder $container) /** * {@inheritdoc} */ - public function getConfiguration(array $config, ContainerBuilder $container) + public function getConfiguration(array $config, ContainerBuilder $container): ?ConfigurationInterface { return new Configuration($this->customConfig); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php index b8b53c25044cd..177c400c89031 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/DependencyInjection/TranslationDebugPass.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml index 3cbdf944af3bb..155871fc278ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/Resources/config/routing.yml @@ -18,6 +18,10 @@ session_setflash: path: /session_setflash/{message} defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::setFlashAction } +injected_flashbag_session_setflash: + path: injected_flashbag/session_setflash/{message} + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController::setFlashAction} + session_showflash: path: /session_showflash defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SessionController::showFlashAction } @@ -52,3 +56,7 @@ fragment_inlined: array_controller: path: /array_controller defaults: { _controller: [ArrayController, someAction] } + +send_email: + path: /send_email + defaults: { _controller: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\EmailController::indexAction } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php index d90041213ce31..db4b2504aa3ec 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestBundle.php @@ -14,8 +14,11 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\AnnotationReaderPass; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\Config\CustomConfig; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\DependencyInjection\TranslationDebugPass; +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ParameterBag\FrozenParameterBag; use Symfony\Component\HttpKernel\Bundle\Bundle; class TestBundle extends Bundle @@ -27,9 +30,25 @@ public function build(ContainerBuilder $container) /** @var $extension DependencyInjection\TestExtension */ $extension = $container->getExtension('test'); + if (!$container->getParameterBag() instanceof FrozenParameterBag) { + $container->setParameter('container.build_hash', 'test_bundle'); + $container->setParameter('container.build_time', time()); + $container->setParameter('container.build_id', 'test_bundle'); + } + $extension->setCustomConfig(new CustomConfig()); $container->addCompilerPass(new AnnotationReaderPass(), PassConfig::TYPE_AFTER_REMOVING); $container->addCompilerPass(new TranslationDebugPass()); + + $container->addCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container) + { + $container->removeDefinition('twig.controller.exception'); + $container->removeDefinition('twig.controller.preview_error'); + } + }); + + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/NonPublicService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/NonPublicService.php index 5c9261ac77bba..284cfd073b242 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/NonPublicService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/NonPublicService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer; class NonPublicService diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PrivateService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PrivateService.php index 6c7e05e532210..6a244cb40ce54 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PrivateService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PrivateService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer; class PrivateService diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PublicService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PublicService.php index 1ea7b0b0ae180..14de890b630fe 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PublicService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/PublicService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer; class PublicService diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/UnusedPrivateService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/UnusedPrivateService.php index 25a7244a50158..3becdba88b78b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/UnusedPrivateService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TestServiceContainer/UnusedPrivateService.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer; class UnusedPrivateService diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php index 449b35f5e9450..5738ab094b34c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Bundle/TestBundle/TransDebug/TransSubscriberService.php @@ -24,7 +24,7 @@ public function __construct(ContainerInterface $container) $this->container = $container; } - public static function getSubscribedServices() + public static function getSubscribedServices(): array { return ['translator' => TranslatorInterface::class]; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.php new file mode 100644 index 0000000000000..f447300c2c69c --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/BundlePathsTest.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\Bundle\FrameworkBundle\Tests\Functional; + +use Symfony\Bundle\FrameworkBundle\Command\AssetsInstallCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\Entity\LegacyPerson; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\Entity\ModernPerson; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Filesystem\Filesystem; + +class BundlePathsTest extends AbstractWebTestCase +{ + public function testBundlePublicDir() + { + $kernel = static::bootKernel(['test_case' => 'BundlePaths']); + $projectDir = sys_get_temp_dir().'/'.uniqid('sf_bundle_paths', true); + + $fs = new Filesystem(); + $fs->mkdir($projectDir.'/public'); + $command = (new Application($kernel))->add(new AssetsInstallCommand($fs, $projectDir)); + $exitCode = (new CommandTester($command))->execute(['target' => $projectDir.'/public']); + + $this->assertSame(0, $exitCode); + $this->assertFileExists($projectDir.'/public/bundles/modern/modern.css'); + $this->assertFileExists($projectDir.'/public/bundles/legacy/legacy.css'); + + $fs->remove($projectDir); + } + + public function testBundleTwigTemplatesDir() + { + static::bootKernel(['test_case' => 'BundlePaths']); + $twig = static::$container->get('twig'); + $bundlesMetadata = static::$container->getParameter('kernel.bundles_metadata'); + + $this->assertSame([$bundlesMetadata['LegacyBundle']['path'].'/Resources/views'], $twig->getLoader()->getPaths('Legacy')); + $this->assertSame("OK\n", $twig->render('@Legacy/index.html.twig')); + + $this->assertSame([$bundlesMetadata['ModernBundle']['path'].'/templates'], $twig->getLoader()->getPaths('Modern')); + $this->assertSame("OK\n", $twig->render('@Modern/index.html.twig')); + } + + public function testBundleTranslationsDir() + { + static::bootKernel(['test_case' => 'BundlePaths']); + $translator = static::$container->get('translator'); + + $this->assertSame('OK', $translator->trans('ok_label', [], 'legacy')); + $this->assertSame('OK', $translator->trans('ok_label', [], 'modern')); + } + + public function testBundleValidationConfigDir() + { + static::bootKernel(['test_case' => 'BundlePaths']); + $validator = static::$container->get('validator'); + + $this->assertTrue($validator->hasMetadataFor(LegacyPerson::class)); + $this->assertCount(1, $constraintViolationList = $validator->validate(new LegacyPerson('john', 5))); + $this->assertSame('This value should be greater than 18.', $constraintViolationList->get(0)->getMessage()); + + $this->assertTrue($validator->hasMetadataFor(ModernPerson::class)); + $this->assertCount(1, $constraintViolationList = $validator->validate(new ModernPerson('john', 5))); + $this->assertSame('This value should be greater than 18.', $constraintViolationList->get(0)->getMessage()); + } + + public function testBundleSerializationConfigDir() + { + static::bootKernel(['test_case' => 'BundlePaths']); + $serializer = static::$container->get('serializer'); + + $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new LegacyPerson('john', 5), 'json')); + $this->assertEquals(['full_name' => 'john', 'age' => 5], $serializer->normalize(new ModernPerson('john', 5), 'json')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php index af57ec9ad9eca..a3a0b23136137 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolClearCommandTest.php @@ -14,13 +14,14 @@ use Symfony\Bundle\FrameworkBundle\Command\CachePoolClearCommand; use Symfony\Bundle\FrameworkBundle\Console\Application; use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; /** * @group functional */ -class CachePoolClearCommandTest extends WebTestCase +class CachePoolClearCommandTest extends AbstractWebTestCase { - protected function setUp() + protected function setUp(): void { static::bootKernel(['test_case' => 'CachePoolClear', 'root_config' => 'config.yml']); } @@ -31,8 +32,8 @@ public function testClearPrivatePool() $tester->execute(['pools' => ['cache.private_pool']], ['decorated' => false]); $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); - $this->assertContains('Clearing cache pool: cache.private_pool', $tester->getDisplay()); - $this->assertContains('[OK] Cache was successfully cleared.', $tester->getDisplay()); + $this->assertStringContainsString('Clearing cache pool: cache.private_pool', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } public function testClearPublicPool() @@ -41,8 +42,8 @@ public function testClearPublicPool() $tester->execute(['pools' => ['cache.public_pool']], ['decorated' => false]); $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); - $this->assertContains('Clearing cache pool: cache.public_pool', $tester->getDisplay()); - $this->assertContains('[OK] Cache was successfully cleared.', $tester->getDisplay()); + $this->assertStringContainsString('Clearing cache pool: cache.public_pool', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } public function testClearPoolWithCustomClearer() @@ -51,8 +52,8 @@ public function testClearPoolWithCustomClearer() $tester->execute(['pools' => ['cache.pool_with_clearer']], ['decorated' => false]); $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); - $this->assertContains('Clearing cache pool: cache.pool_with_clearer', $tester->getDisplay()); - $this->assertContains('[OK] Cache was successfully cleared.', $tester->getDisplay()); + $this->assertStringContainsString('Clearing cache pool: cache.pool_with_clearer', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } public function testCallClearer() @@ -61,16 +62,14 @@ public function testCallClearer() $tester->execute(['pools' => ['cache.app_clearer']], ['decorated' => false]); $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:clear exits with 0 in case of success'); - $this->assertContains('Calling cache clearer: cache.app_clearer', $tester->getDisplay()); - $this->assertContains('[OK] Cache was successfully cleared.', $tester->getDisplay()); + $this->assertStringContainsString('Calling cache clearer: cache.app_clearer', $tester->getDisplay()); + $this->assertStringContainsString('[OK] Cache was successfully cleared.', $tester->getDisplay()); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException - * @expectedExceptionMessage You have requested a non-existent service "unknown_pool" - */ public function testClearUnexistingPool() { + $this->expectException(ServiceNotFoundException::class); + $this->expectExceptionMessage('You have requested a non-existent service "unknown_pool"'); $this->createCommandTester() ->execute(['pools' => ['unknown_pool']], ['decorated' => false]); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php index 15e7994e46002..d7e5aae80c4f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolListCommandTest.php @@ -18,9 +18,9 @@ /** * @group functional */ -class CachePoolListCommandTest extends WebTestCase +class CachePoolListCommandTest extends AbstractWebTestCase { - protected function setUp() + protected function setUp(): void { static::bootKernel(['test_case' => 'CachePools', 'root_config' => 'config.yml']); } @@ -31,8 +31,8 @@ public function testListPools() $tester->execute([]); $this->assertSame(0, $tester->getStatusCode(), 'cache:pool:list exits with 0 in case of success'); - $this->assertContains('cache.app', $tester->getDisplay()); - $this->assertContains('cache.system', $tester->getDisplay()); + $this->assertStringContainsString('cache.app', $tester->getDisplay()); + $this->assertStringContainsString('cache.system', $tester->getDisplay()); } public function testEmptyList() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php index e970e17c6ebdb..fc53e26661993 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/CachePoolsTest.php @@ -15,8 +15,9 @@ use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Exception\InvalidArgumentException; +use Symfony\Component\HttpKernel\KernelInterface; -class CachePoolsTest extends WebTestCase +class CachePoolsTest extends AbstractWebTestCase { public function testCachePools() { @@ -25,23 +26,21 @@ public function testCachePools() /** * @requires extension redis + * @group integration */ public function testRedisCachePools() { + $this->skipIfRedisUnavailable(); + try { $this->doTestCachePools(['root_config' => 'redis_config.yml', 'environment' => 'redis_cache'], RedisAdapter::class); } catch (\PHPUnit\Framework\Error\Warning $e) { - if (0 !== strpos($e->getMessage(), 'unable to connect to')) { - throw $e; - } - $this->markTestSkipped($e->getMessage()); - } catch (\PHPUnit_Framework_Error_Warning $e) { - if (0 !== strpos($e->getMessage(), 'unable to connect to')) { + if (!str_starts_with($e->getMessage(), 'unable to connect to')) { throw $e; } $this->markTestSkipped($e->getMessage()); } catch (InvalidArgumentException $e) { - if (0 !== strpos($e->getMessage(), 'Redis connection failed')) { + if (!str_starts_with($e->getMessage(), 'Redis connection ')) { throw $e; } $this->markTestSkipped($e->getMessage()); @@ -50,18 +49,16 @@ public function testRedisCachePools() /** * @requires extension redis + * @group integration */ public function testRedisCustomCachePools() { + $this->skipIfRedisUnavailable(); + try { $this->doTestCachePools(['root_config' => 'redis_custom_config.yml', 'environment' => 'custom_redis_cache'], RedisAdapter::class); } catch (\PHPUnit\Framework\Error\Warning $e) { - if (0 !== strpos($e->getMessage(), 'unable to connect to')) { - throw $e; - } - $this->markTestSkipped($e->getMessage()); - } catch (\PHPUnit_Framework_Error_Warning $e) { - if (0 !== strpos($e->getMessage(), 'unable to connect to')) { + if (!str_starts_with($e->getMessage(), 'unable to connect to')) { throw $e; } $this->markTestSkipped($e->getMessage()); @@ -116,8 +113,17 @@ private function doTestCachePools($options, $adapterClass) $this->assertNotInstanceof(TagAwareAdapter::class, $pool7); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return parent::createKernel(['test_case' => 'CachePools'] + $options); } + + private function skipIfRedisUnavailable() + { + try { + (new \Redis())->connect(...explode(':', getenv('REDIS_HOST'))); + } catch (\Exception $e) { + self::markTestSkipped($e->getMessage()); + } + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php index f52d48e89904f..fbfa0be6e6c08 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDebugCommandTest.php @@ -19,11 +19,11 @@ /** * @group functional */ -class ConfigDebugCommandTest extends WebTestCase +class ConfigDebugCommandTest extends AbstractWebTestCase { private $application; - protected function setUp() + protected function setUp(): void { $kernel = static::createKernel(['test_case' => 'ConfigDump', 'root_config' => 'config.yml']); $this->application = new Application($kernel); @@ -36,7 +36,7 @@ public function testDumpBundleName() $ret = $tester->execute(['name' => 'TestBundle']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('custom: foo', $tester->getDisplay()); + $this->assertStringContainsString('custom: foo', $tester->getDisplay()); } public function testDumpBundleOption() @@ -45,7 +45,7 @@ public function testDumpBundleOption() $ret = $tester->execute(['name' => 'TestBundle', 'path' => 'custom']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('foo', $tester->getDisplay()); + $this->assertStringContainsString('foo', $tester->getDisplay()); } public function testParametersValuesAreResolved() @@ -54,8 +54,27 @@ public function testParametersValuesAreResolved() $ret = $tester->execute(['name' => 'framework']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains("locale: '%env(LOCALE)%'", $tester->getDisplay()); - $this->assertContains('secret: test', $tester->getDisplay()); + $this->assertStringContainsString("locale: '%env(LOCALE)%'", $tester->getDisplay()); + $this->assertStringContainsString('secret: test', $tester->getDisplay()); + } + + public function testDefaultParameterValueIsResolvedIfConfigIsExisting() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'framework']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $kernelCacheDir = $this->application->getKernel()->getContainer()->getParameter('kernel.cache_dir'); + $this->assertStringContainsString(sprintf("dsn: 'file:%s/profiler'", $kernelCacheDir), $tester->getDisplay()); + } + + public function testDumpExtensionConfigWithoutBundle() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'test_dump']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('enabled: true', $tester->getDisplay()); } public function testDumpUndefinedBundleOption() @@ -63,7 +82,7 @@ public function testDumpUndefinedBundleOption() $tester = $this->createCommandTester(); $tester->execute(['name' => 'TestBundle', 'path' => 'foo']); - $this->assertContains('Unable to find configuration for "test.foo"', $tester->getDisplay()); + $this->assertStringContainsString('Unable to find configuration for "test.foo"', $tester->getDisplay()); } public function testDumpWithPrefixedEnv() @@ -71,13 +90,37 @@ public function testDumpWithPrefixedEnv() $tester = $this->createCommandTester(); $tester->execute(['name' => 'FrameworkBundle']); - $this->assertContains("cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'", $tester->getDisplay()); + $this->assertStringContainsString("cookie_httponly: '%env(bool:COOKIE_HTTPONLY)%'", $tester->getDisplay()); + } + + public function testDumpFallsBackToDefaultConfigAndResolvesParameterValue() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'DefaultConfigTestBundle']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('foo: bar', $tester->getDisplay()); + } + + public function testDumpFallsBackToDefaultConfigAndResolvesEnvPlaceholder() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'DefaultConfigTestBundle']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString("baz: '%env(BAZ)%'", $tester->getDisplay()); + } + + public function testDumpThrowsExceptionWhenDefaultConfigFallbackIsImpossible() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The extension with alias "extension_without_config_test" does not have configuration.'); + + $tester = $this->createCommandTester(); + $tester->execute(['name' => 'ExtensionWithoutConfigTestBundle']); } - /** - * @return CommandTester - */ - private function createCommandTester() + private function createCommandTester(): CommandTester { $command = $this->application->find('debug:config'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php index a4cfd6cfa9b7d..74c1889e2b99a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ConfigDumpReferenceCommandTest.php @@ -19,11 +19,11 @@ /** * @group functional */ -class ConfigDumpReferenceCommandTest extends WebTestCase +class ConfigDumpReferenceCommandTest extends AbstractWebTestCase { private $application; - protected function setUp() + protected function setUp(): void { $kernel = static::createKernel(['test_case' => 'ConfigDump', 'root_config' => 'config.yml']); $this->application = new Application($kernel); @@ -36,8 +36,17 @@ public function testDumpBundleName() $ret = $tester->execute(['name' => 'TestBundle']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('test:', $tester->getDisplay()); - $this->assertContains(' custom:', $tester->getDisplay()); + $this->assertStringContainsString('test:', $tester->getDisplay()); + $this->assertStringContainsString(' custom:', $tester->getDisplay()); + } + + public function testDumpExtensionConfigWithoutBundle() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'test_dump']); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringContainsString('enabled: true', $tester->getDisplay()); } public function testDumpAtPath() @@ -70,13 +79,10 @@ public function testDumpAtPathXml() ]); $this->assertSame(1, $ret); - $this->assertContains('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); + $this->assertStringContainsString('[ERROR] The "path" option is only available for the "yaml" format.', $tester->getDisplay()); } - /** - * @return CommandTester - */ - private function createCommandTester() + private function createCommandTester(): CommandTester { $command = $this->application->find('config:dump-reference'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php index 88cb8b28e8ecb..70b7bfcaafc5d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDebugCommandTest.php @@ -18,7 +18,7 @@ /** * @group functional */ -class ContainerDebugCommandTest extends WebTestCase +class ContainerDebugCommandTest extends AbstractWebTestCase { public function testDumpContainerIfNotExists() { @@ -45,7 +45,7 @@ public function testNoDebug() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container']); - $this->assertContains('public', $tester->getDisplay()); + $this->assertStringContainsString('public', $tester->getDisplay()); } public function testPrivateAlias() @@ -57,15 +57,15 @@ public function testPrivateAlias() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--show-hidden' => true]); - $this->assertNotContains('public', $tester->getDisplay()); - $this->assertNotContains('private_alias', $tester->getDisplay()); + $this->assertStringNotContainsString('public', $tester->getDisplay()); + $this->assertStringNotContainsString('private_alias', $tester->getDisplay()); $tester->run(['command' => 'debug:container']); - $this->assertContains('public', $tester->getDisplay()); - $this->assertContains('private_alias', $tester->getDisplay()); + $this->assertStringContainsString('public', $tester->getDisplay()); + $this->assertStringContainsString('private_alias', $tester->getDisplay()); $tester->run(['command' => 'debug:container', 'name' => 'private_alias']); - $this->assertContains('The "private_alias" service or alias has been removed', $tester->getDisplay()); + $this->assertStringContainsString('The "private_alias" service or alias has been removed', $tester->getDisplay()); } /** @@ -80,7 +80,7 @@ public function testIgnoreBackslashWhenFindingService(string $validServiceId) $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', 'name' => $validServiceId]); - $this->assertNotContains('No services found', $tester->getDisplay()); + $this->assertStringNotContainsString('No services found', $tester->getDisplay()); } public function testDescribeEnvVars() @@ -116,7 +116,7 @@ public function testDescribeEnvVars() * UNKNOWN TXT - , $tester->getDisplay(true)); + , $tester->getDisplay(true)); putenv('REAL'); } @@ -133,7 +133,7 @@ public function testDescribeEnvVar() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:container', '--env-var' => 'js'], ['decorated' => false]); - $this->assertContains(file_get_contents(__DIR__.'/Fixtures/describe_env_vars.txt'), $tester->getDisplay(true)); + $this->assertStringContainsString(file_get_contents(__DIR__.'/Fixtures/describe_env_vars.txt'), $tester->getDisplay(true)); } public function provideIgnoreBackslashWhenFindingService() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php index fe0cea4d16a85..f543058440582 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ContainerDumpTest.php @@ -14,18 +14,18 @@ /** * Checks that the container compiles correctly when all the bundle features are enabled. */ -class ContainerDumpTest extends WebTestCase +class ContainerDumpTest extends AbstractWebTestCase { public function testContainerCompilationInDebug() { - $client = $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml']); + $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml']); $this->assertTrue(static::$container->has('serializer')); } public function testContainerCompilation() { - $client = $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml', 'debug' => false]); + $this->createClient(['test_case' => 'ContainerDump', 'root_config' => 'config.yml', 'debug' => false]); $this->assertTrue(static::$container->has('serializer')); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php index c468a2a4da70c..a0ade821d5165 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/DebugAutowiringCommandTest.php @@ -17,7 +17,7 @@ /** * @group functional */ -class DebugAutowiringCommandTest extends WebTestCase +class DebugAutowiringCommandTest extends AbstractWebTestCase { public function testBasicFunctionality() { @@ -29,8 +29,8 @@ public function testBasicFunctionality() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring']); - $this->assertContains('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); - $this->assertContains('(http_kernel)', $tester->getDisplay()); + $this->assertStringContainsString('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); + $this->assertStringContainsString('(http_kernel)', $tester->getDisplay()); } public function testSearchArgument() @@ -43,8 +43,8 @@ public function testSearchArgument() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring', 'search' => 'kern']); - $this->assertContains('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); - $this->assertNotContains('Symfony\Component\Routing\RouterInterface', $tester->getDisplay()); + $this->assertStringContainsString('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); + $this->assertStringNotContainsString('Symfony\Component\Routing\RouterInterface', $tester->getDisplay()); } public function testSearchIgnoreBackslashWhenFindingService() @@ -56,7 +56,7 @@ public function testSearchIgnoreBackslashWhenFindingService() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring', 'search' => 'HttpKernelHttpKernelInterface']); - $this->assertContains('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); + $this->assertStringContainsString('Symfony\Component\HttpKernel\HttpKernelInterface', $tester->getDisplay()); } public function testSearchNoResults() @@ -69,7 +69,7 @@ public function testSearchNoResults() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring', 'search' => 'foo_fake'], ['capture_stderr_separately' => true]); - $this->assertContains('No autowirable classes or interfaces found matching "foo_fake"', $tester->getErrorOutput()); + $this->assertStringContainsString('No autowirable classes or interfaces found matching "foo_fake"', $tester->getErrorOutput()); $this->assertEquals(1, $tester->getStatusCode()); } @@ -83,7 +83,7 @@ public function testSearchNotAliasedService() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring', 'search' => 'redirect']); - $this->assertContains(' more concrete service would be displayed when adding the "--all" option.', $tester->getDisplay()); + $this->assertStringContainsString(' more concrete service would be displayed when adding the "--all" option.', $tester->getDisplay()); } public function testSearchNotAliasedServiceWithAll() @@ -95,6 +95,18 @@ public function testSearchNotAliasedServiceWithAll() $tester = new ApplicationTester($application); $tester->run(['command' => 'debug:autowiring', 'search' => 'redirect', '--all' => true]); - $this->assertContains('Pro-tip: use interfaces in your type-hints instead of classes to benefit from the dependency inversion principle.', $tester->getDisplay()); + $this->assertStringContainsString('Pro-tip: use interfaces in your type-hints instead of classes to benefit from the dependency inversion principle.', $tester->getDisplay()); + } + + public function testNotConfusedByClassAliases() + { + static::bootKernel(['test_case' => 'ContainerDebug', 'root_config' => 'config.yml']); + + $application = new Application(static::$kernel); + $application->setAutoExit(false); + + $tester = new ApplicationTester($application); + $tester->run(['command' => 'debug:autowiring', 'search' => 'ClassAlias']); + $this->assertStringContainsString('Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ClassAliasExampleClass', $tester->getDisplay()); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.php new file mode 100644 index 0000000000000..d8cef92850992 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/Extension/TestDumpExtension.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\Bundle\FrameworkBundle\Tests\Functional\Extension; + +use Symfony\Component\Config\Definition\Builder\TreeBuilder; +use Symfony\Component\Config\Definition\ConfigurationInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\Extension; + +class TestDumpExtension extends Extension implements ConfigurationInterface +{ + public function getConfigTreeBuilder(): TreeBuilder + { + $treeBuilder = new TreeBuilder('test_dump'); + $treeBuilder->getRootNode() + ->children() + ->booleanNode('enabled')->defaultTrue()->end() + ->end() + ; + + return $treeBuilder; + } + + public function load(array $configs, ContainerBuilder $container): void + { + } + + public function getConfiguration(array $config, ContainerBuilder $container): ConfigurationInterface + { + return $this; + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php index d3dbeb765bf6b..a4ac17238a4b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class FragmentTest extends WebTestCase +class FragmentTest extends AbstractWebTestCase { /** * @dataProvider getConfigs diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php index 85987fe28f6d1..efaecd6c89519 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/MailerTest.php @@ -1,8 +1,18 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; use Psr\Log\LoggerInterface; +use Symfony\Bundle\FullStack; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Mailer\SentMessage; @@ -10,7 +20,7 @@ use Symfony\Component\Mime\Address; use Symfony\Component\Mime\Email; -class MailerTest extends WebTestCase +class MailerTest extends AbstractWebTestCase { public function testEnvelopeListener() { @@ -42,6 +52,11 @@ public function __construct(EventDispatcherInterface $eventDispatcher, LoggerInt $this->onDoSend = $onDoSend; } + public function __toString(): string + { + return 'dummy://local'; + } + protected function doSend(SentMessage $message): void { $onDoSend = $this->onDoSend; @@ -49,7 +64,7 @@ protected function doSend(SentMessage $message): void } }; - $mailer = new Mailer($testTransport, null); + $mailer = new Mailer($testTransport); $message = (new Email()) ->subject('Test subject') @@ -59,4 +74,38 @@ protected function doSend(SentMessage $message): void $mailer->send($message); } + + public function testMailerAssertions() + { + $client = $this->createClient(['test_case' => 'Mailer', 'root_config' => 'config.yml', 'debug' => true]); + $client->request('GET', '/send_email'); + + $this->assertEmailCount(2); + $first = 0; + $second = 1; + if (!class_exists(FullStack::class)) { + $this->assertQueuedEmailCount(2); + $first = 1; + $second = 3; + $this->assertEmailIsQueued($this->getMailerEvent(0)); + $this->assertEmailIsQueued($this->getMailerEvent(2)); + } + $this->assertEmailIsNotQueued($this->getMailerEvent($first)); + $this->assertEmailIsNotQueued($this->getMailerEvent($second)); + + $email = $this->getMailerMessage($first); + $this->assertEmailHasHeader($email, 'To'); + $this->assertEmailHeaderSame($email, 'To', 'fabien@symfony.com'); + $this->assertEmailHeaderNotSame($email, 'To', 'helene@symfony.com'); + $this->assertEmailTextBodyContains($email, 'Bar'); + $this->assertEmailTextBodyNotContains($email, 'Foo'); + $this->assertEmailHtmlBodyContains($email, 'Foo'); + $this->assertEmailHtmlBodyNotContains($email, 'Bar'); + $this->assertEmailAttachmentCount($email, 1); + + $email = $this->getMailerMessage($second); + $this->assertEmailAddressContains($email, 'To', 'fabien@symfony.com'); + $this->assertEmailAddressContains($email, 'To', 'thomas@symfony.com'); + $this->assertEmailAddressContains($email, 'Reply-To', 'me@symfony.com'); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php index 2768b59a1c3f5..35c2e63b7e04a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/ProfilerTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class ProfilerTest extends WebTestCase +class ProfilerTest extends AbstractWebTestCase { /** * @dataProvider getConfigs @@ -24,16 +24,16 @@ public function testProfilerIsDisabled($insulate) } $client->request('GET', '/profiler'); - $this->assertFalse($client->getProfile()); + $this->assertNull($client->getProfile()); // enable the profiler for the next request $client->enableProfiler(); - $this->assertFalse($client->getProfile()); + $this->assertNull($client->getProfile()); $client->request('GET', '/profiler'); - $this->assertInternalType('object', $client->getProfile()); + $this->assertIsObject($client->getProfile()); $client->request('GET', '/profiler'); - $this->assertFalse($client->getProfile()); + $this->assertNull($client->getProfile()); } public function getConfigs() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php index 61669e90adbc7..d9821820c04e9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/PropertyInfoTest.php @@ -13,7 +13,7 @@ use Symfony\Component\PropertyInfo\Type; -class PropertyInfoTest extends WebTestCase +class PropertyInfoTest extends AbstractWebTestCase { public function testPhpDocPriority() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php index 73f84f842f83f..161ec62eb683a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/RouterDebugCommandTest.php @@ -17,11 +17,11 @@ /** * @group functional */ -class RouterDebugCommandTest extends WebTestCase +class RouterDebugCommandTest extends AbstractWebTestCase { private $application; - protected function setUp() + protected function setUp(): void { $kernel = static::createKernel(['test_case' => 'RouterDebug', 'root_config' => 'config.yml']); $this->application = new Application($kernel); @@ -34,9 +34,9 @@ public function testDumpAllRoutes() $display = $tester->getDisplay(); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('routerdebug_test', $display); - $this->assertContains('/test', $display); - $this->assertContains('/session', $display); + $this->assertStringContainsString('routerdebug_test', $display); + $this->assertStringContainsString('/test', $display); + $this->assertStringContainsString('/session', $display); } public function testDumpOneRoute() @@ -45,8 +45,8 @@ public function testDumpOneRoute() $ret = $tester->execute(['name' => 'routerdebug_session_welcome']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('routerdebug_session_welcome', $tester->getDisplay()); - $this->assertContains('/session', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_session_welcome', $tester->getDisplay()); + $this->assertStringContainsString('/session', $tester->getDisplay()); } public function testSearchMultipleRoutes() @@ -56,17 +56,32 @@ public function testSearchMultipleRoutes() $ret = $tester->execute(['name' => 'routerdebug'], ['interactive' => true]); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('Select one of the matching routes:', $tester->getDisplay()); - $this->assertContains('routerdebug_test', $tester->getDisplay()); - $this->assertContains('/test', $tester->getDisplay()); + $this->assertStringContainsString('Select one of the matching routes:', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_test', $tester->getDisplay()); + $this->assertStringContainsString('/test', $tester->getDisplay()); + } + + public function testSearchMultipleRoutesWithoutInteraction() + { + $tester = $this->createCommandTester(); + $ret = $tester->execute(['name' => 'routerdebug'], ['interactive' => false]); + + $this->assertSame(0, $ret, 'Returns 0 in case of success'); + $this->assertStringNotContainsString('Select one of the matching routes:', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_session_welcome', $tester->getDisplay()); + $this->assertStringContainsString('/session', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_session_welcome_name', $tester->getDisplay()); + $this->assertStringContainsString('/session/{name} ', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_session_logout', $tester->getDisplay()); + $this->assertStringContainsString('/session_logout', $tester->getDisplay()); + $this->assertStringContainsString('routerdebug_test', $tester->getDisplay()); + $this->assertStringContainsString('/test', $tester->getDisplay()); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage The route "gerard" does not exist. - */ public function testSearchWithThrow() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The route "gerard" does not exist.'); $tester = $this->createCommandTester(); $tester->execute(['name' => 'gerard'], ['interactive' => true]); } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php index 0a92fd2f91ce5..b0d774bdd55e8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SerializerTest.php @@ -14,7 +14,7 @@ /** * @author KΓ©vin Dunglas */ -class SerializerTest extends WebTestCase +class SerializerTest extends AbstractWebTestCase { public function testDeserializeArrayOfObject() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php index 2924c39ce1a01..530492ab8b4ed 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SessionTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class SessionTest extends WebTestCase +class SessionTest extends AbstractWebTestCase { /** * Tests session attributes persist. @@ -27,23 +27,23 @@ public function testWelcome($config, $insulate) // no session $crawler = $client->request('GET', '/session'); - $this->assertContains('You are new here and gave no name.', $crawler->text()); + $this->assertStringContainsString('You are new here and gave no name.', $crawler->text()); // remember name $crawler = $client->request('GET', '/session/drak'); - $this->assertContains('Hello drak, nice to meet you.', $crawler->text()); + $this->assertStringContainsString('Hello drak, nice to meet you.', $crawler->text()); // prove remembered name $crawler = $client->request('GET', '/session'); - $this->assertContains('Welcome back drak, nice to meet you.', $crawler->text()); + $this->assertStringContainsString('Welcome back drak, nice to meet you.', $crawler->text()); // clear session $crawler = $client->request('GET', '/session_logout'); - $this->assertContains('Session cleared.', $crawler->text()); + $this->assertStringContainsString('Session cleared.', $crawler->text()); // prove cleared session $crawler = $client->request('GET', '/session'); - $this->assertContains('You are new here and gave no name.', $crawler->text()); + $this->assertStringContainsString('You are new here and gave no name.', $crawler->text()); } /** @@ -59,14 +59,37 @@ public function testFlash($config, $insulate) } // set flash - $crawler = $client->request('GET', '/session_setflash/Hello%20world.'); + $client->request('GET', '/session_setflash/Hello%20world.'); // check flash displays on redirect - $this->assertContains('Hello world.', $client->followRedirect()->text()); + $this->assertStringContainsString('Hello world.', $client->followRedirect()->text()); // check flash is gone $crawler = $client->request('GET', '/session_showflash'); - $this->assertContains('No flash was set.', $crawler->text()); + $this->assertStringContainsString('No flash was set.', $crawler->text()); + } + + /** + * Tests flash messages work when flashbag service is injected to the constructor. + * + * @dataProvider getConfigs + */ + public function testFlashOnInjectedFlashbag($config, $insulate) + { + $client = $this->createClient(['test_case' => 'Session', 'root_config' => $config]); + if ($insulate) { + $client->insulate(); + } + + // set flash + $client->request('GET', '/injected_flashbag/session_setflash/Hello%20world.'); + + // check flash displays on redirect + $this->assertStringContainsString('Hello world.', $client->followRedirect()->text()); + + // check flash is gone + $crawler = $client->request('GET', '/session_showflash'); + $this->assertStringContainsString('No flash was set.', $crawler->text()); } /** @@ -93,39 +116,39 @@ public function testTwoClients($config, $insulate) // new session, so no name set. $crawler1 = $client1->request('GET', '/session'); - $this->assertContains('You are new here and gave no name.', $crawler1->text()); + $this->assertStringContainsString('You are new here and gave no name.', $crawler1->text()); // set name of client1 $crawler1 = $client1->request('GET', '/session/client1'); - $this->assertContains('Hello client1, nice to meet you.', $crawler1->text()); + $this->assertStringContainsString('Hello client1, nice to meet you.', $crawler1->text()); // no session for client2 $crawler2 = $client2->request('GET', '/session'); - $this->assertContains('You are new here and gave no name.', $crawler2->text()); + $this->assertStringContainsString('You are new here and gave no name.', $crawler2->text()); // remember name client2 $crawler2 = $client2->request('GET', '/session/client2'); - $this->assertContains('Hello client2, nice to meet you.', $crawler2->text()); + $this->assertStringContainsString('Hello client2, nice to meet you.', $crawler2->text()); // prove remembered name of client1 $crawler1 = $client1->request('GET', '/session'); - $this->assertContains('Welcome back client1, nice to meet you.', $crawler1->text()); + $this->assertStringContainsString('Welcome back client1, nice to meet you.', $crawler1->text()); // prove remembered name of client2 $crawler2 = $client2->request('GET', '/session'); - $this->assertContains('Welcome back client2, nice to meet you.', $crawler2->text()); + $this->assertStringContainsString('Welcome back client2, nice to meet you.', $crawler2->text()); // clear client1 $crawler1 = $client1->request('GET', '/session_logout'); - $this->assertContains('Session cleared.', $crawler1->text()); + $this->assertStringContainsString('Session cleared.', $crawler1->text()); // prove client1 data is cleared $crawler1 = $client1->request('GET', '/session'); - $this->assertContains('You are new here and gave no name.', $crawler1->text()); + $this->assertStringContainsString('You are new here and gave no name.', $crawler1->text()); // prove remembered name of client 10000 2 remains untouched. $crawler2 = $client2->request('GET', '/session'); - $this->assertContains('Welcome back client2, nice to meet you.', $crawler2->text()); + $this->assertStringContainsString('Welcome back client2, nice to meet you.', $crawler2->text()); } /** diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php index 9d040581db50f..d32b6b7b121c5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/SubRequestsTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional; -class SubRequestsTest extends WebTestCase +class SubRequestsTest extends AbstractWebTestCase { public function testStateAfterSubRequest() { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php index baa12ab2d1675..4122a749dfd1d 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TestServiceContainerTest.php @@ -18,7 +18,7 @@ use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestServiceContainer\UnusedPrivateService; use Symfony\Component\DependencyInjection\ContainerInterface; -class TestServiceContainerTest extends WebTestCase +class TestServiceContainerTest extends AbstractWebTestCase { public function testThatPrivateServicesAreUnavailableIfTestConfigIsDisabled() { @@ -44,4 +44,22 @@ public function testThatPrivateServicesAreAvailableIfTestConfigIsEnabled() $this->assertTrue(static::$container->has('private_service')); $this->assertFalse(static::$container->has(UnusedPrivateService::class)); } + + /** + * @doesNotPerformAssertions + */ + public function testBootKernel() + { + static::bootKernel(['test_case' => 'TestServiceContainer']); + } + + /** + * @depends testBootKernel + */ + public function testKernelIsNotInitialized() + { + self::assertNull(self::$class); + self::assertNull(self::$kernel); + self::assertFalse(self::$booted); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php index 1da49ce79c8f6..382c4b5d94731 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/TranslationDebugCommandTest.php @@ -17,11 +17,11 @@ /** * @group functional */ -class TranslationDebugCommandTest extends WebTestCase +class TranslationDebugCommandTest extends AbstractWebTestCase { private $application; - protected function setUp() + protected function setUp(): void { $kernel = static::createKernel(['test_case' => 'TransDebug', 'root_config' => 'config.yml']); $this->application = new Application($kernel); @@ -33,13 +33,13 @@ public function testDumpAllTrans() $ret = $tester->execute(['locale' => 'en']); $this->assertSame(0, $ret, 'Returns 0 in case of success'); - $this->assertContains('missing messages hello_from_construct_arg_service', $tester->getDisplay()); - $this->assertContains('missing messages hello_from_subscriber_service', $tester->getDisplay()); - $this->assertContains('missing messages hello_from_property_service', $tester->getDisplay()); - $this->assertContains('missing messages hello_from_method_calls_service', $tester->getDisplay()); - $this->assertContains('missing messages hello_from_controller', $tester->getDisplay()); - $this->assertContains('unused validators This value should be blank.', $tester->getDisplay()); - $this->assertContains('unused security Invalid CSRF token.', $tester->getDisplay()); + $this->assertStringContainsString('missing messages hello_from_construct_arg_service', $tester->getDisplay()); + $this->assertStringContainsString('missing messages hello_from_subscriber_service', $tester->getDisplay()); + $this->assertStringContainsString('missing messages hello_from_property_service', $tester->getDisplay()); + $this->assertStringContainsString('missing messages hello_from_method_calls_service', $tester->getDisplay()); + $this->assertStringContainsString('missing messages hello_from_controller', $tester->getDisplay()); + $this->assertStringContainsString('unused validators This value should be blank.', $tester->getDisplay()); + $this->assertStringContainsString('unused security Invalid CSRF token.', $tester->getDisplay()); } private function createCommandTester(): CommandTester diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php index 0c4d03bfc11af..817c9360f4da3 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/AppKernel.php @@ -12,8 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Functional\app; use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Extension\TestDumpExtension; use Symfony\Component\Config\Loader\LoaderInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -45,7 +47,7 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu parent::__construct($environment, $debug); } - public function registerBundles() + public function registerBundles(): iterable { if (!file_exists($filename = $this->getProjectDir().'/'.$this->testCase.'/bundles.php')) { throw new \RuntimeException(sprintf('The bundles file "%s" does not exist.', $filename)); @@ -54,17 +56,17 @@ public function registerBundles() return include $filename; } - public function getProjectDir() + public function getProjectDir(): string { return __DIR__; } - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/'.$this->varDir.'/'.$this->testCase.'/cache/'.$this->environment; } - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/'.$this->varDir.'/'.$this->testCase.'/logs'; } @@ -77,23 +79,39 @@ public function registerContainerConfiguration(LoaderInterface $loader) protected function build(ContainerBuilder $container) { $container->register('logger', NullLogger::class); + $container->registerExtension(new TestDumpExtension()); } - public function __sleep() + public function __sleep(): array { return ['varDir', 'testCase', 'rootConfig', 'environment', 'debug']; } public function __wakeup() { + foreach ($this as $k => $v) { + if (\is_object($v)) { + throw new \BadMethodCallException('Cannot unserialize '.__CLASS__); + } + } + $this->__construct($this->varDir, $this->testCase, $this->rootConfig, $this->environment, $this->debug); } - protected function getKernelParameters() + protected function getKernelParameters(): array { $parameters = parent::getKernelParameters(); $parameters['kernel.test_case'] = $this->testCase; return $parameters; } + + public function getContainer(): ContainerInterface + { + if (!$this->container) { + throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); + } + + return parent::getContainer(); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/bundles.php new file mode 100644 index 0000000000000..7e01199ea7909 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/bundles.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\LegacyBundle\LegacyBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ModernBundle\src\ModernBundle; +use Symfony\Bundle\TwigBundle\TwigBundle; + +return [ + new FrameworkBundle(), + new TwigBundle(), + new ModernBundle(), + new LegacyBundle(), +]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml new file mode 100644 index 0000000000000..3e1e53738cf93 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/BundlePaths/config.yml @@ -0,0 +1,11 @@ +imports: + - { resource: ../config/default.yml } + +framework: + translator: true + validation: true + serializer: true + +twig: + strict_variables: '%kernel.debug%' + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php index 15ff182c6fed5..13ab9fddee4a6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/bundles.php @@ -10,9 +10,7 @@ */ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; -use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; return [ new FrameworkBundle(), - new TestBundle(), ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml index df20c5357f7a4..8681e59a7bb4a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/CachePools/redis_custom_config.yml @@ -8,8 +8,8 @@ services: cache.test_redis_connection: public: false class: Redis - calls: - - [connect, ['%env(REDIS_HOST)%']] + factory: ['Symfony\Component\Cache\Adapter\RedisAdapter', 'createConnection'] + arguments: ['redis://%env(REDIS_HOST)%'] cache.app: parent: cache.adapter.redis diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php index 15ff182c6fed5..c4fb0bbe8ce48 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/bundles.php @@ -10,9 +10,13 @@ */ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\DefaultConfigTestBundle\DefaultConfigTestBundle; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\ExtensionWithoutConfigTestBundle\ExtensionWithoutConfigTestBundle; use Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\TestBundle; return [ + new DefaultConfigTestBundle(), + new ExtensionWithoutConfigTestBundle(), new FrameworkBundle(), new TestBundle(), ]; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml index 432e35bd2f24d..a7a03a31d6602 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ConfigDump/config.yml @@ -11,3 +11,4 @@ parameters: env(LOCALE): en env(COOKIE_HTTPONLY): '1' secret: test + default_config_test_foo: bar diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml index 0cc73dbc81422..25c1c784298b5 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/ContainerDebug/config.yml @@ -13,6 +13,7 @@ services: public: false Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass: class: Symfony\Bundle\FrameworkBundle\Tests\Fixtures\BackslashClass + Symfony\Bundle\FrameworkBundle\Tests\Fixtures\ClassAliasExampleClass: '@public' env: class: stdClass arguments: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml index 16fc81dd268d4..ceeea37a1001b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Fragment/config.yml @@ -7,3 +7,4 @@ framework: twig: strict_variables: '%kernel.debug%' + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml index 196869945eafe..c2c3ace06f179 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/config.yml @@ -1,8 +1,10 @@ imports: - { resource: ../config/default.yml } + - { resource: services.yml } framework: mailer: + dsn: 'null://null' envelope: sender: sender@example.org recipients: diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/routing.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/routing.yml new file mode 100644 index 0000000000000..4fb9a95400e97 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/routing.yml @@ -0,0 +1,2 @@ +_emailtest_bundle: + resource: '@TestBundle/Resources/config/routing.yml' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/services.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/services.yml new file mode 100644 index 0000000000000..4902788bc76cd --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Mailer/services.yml @@ -0,0 +1,6 @@ +services: + _defaults: + public: true + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\EmailController: + tags: ['controller.service_arguments'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml index ad6bdb691ca52..4807c42d1ede8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/app/Session/config.yml @@ -5,3 +5,7 @@ services: Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\SubRequestController: tags: - { name: controller.service_arguments, action: indexAction, argument: handler, id: fragment.handler } + + Symfony\Bundle\FrameworkBundle\Tests\Functional\Bundle\TestBundle\Controller\InjectedFlashbagSessionController: + autowire: true + tags: ['controller.service_arguments'] diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php index e765c6c23b3c7..6eb02fa11a8d9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/ConcreteMicroKernel.php @@ -19,7 +19,7 @@ use Symfony\Component\EventDispatcher\EventSubscriberInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpFoundation\Response; -use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\HttpKernel\KernelEvents; use Symfony\Component\Routing\RouteCollectionBuilder; @@ -30,9 +30,9 @@ class ConcreteMicroKernel extends Kernel implements EventSubscriberInterface private $cacheDir; - public function onKernelException(RequestEvent $event) + public function onKernelException(ExceptionEvent $event) { - if ($event->getException() instanceof Danger) { + if ($event->getThrowable() instanceof Danger) { $event->setResponse(Response::create('It\'s dangerous to go alone. Take this βš”')); } } @@ -47,24 +47,24 @@ public function dangerousAction() throw new Danger(); } - public function registerBundles() + public function registerBundles(): iterable { return [ new FrameworkBundle(), ]; } - public function getCacheDir() + public function getCacheDir(): string { return $this->cacheDir = sys_get_temp_dir().'/sf_micro_kernel'; } - public function getLogDir() + public function getLogDir(): string { return $this->cacheDir; } - public function __sleep() + public function __sleep(): array { throw new \BadMethodCallException('Cannot serialize '.__CLASS__); } @@ -76,8 +76,10 @@ public function __wakeup() public function __destruct() { - $fs = new Filesystem(); - $fs->remove($this->cacheDir); + if ($this->cacheDir) { + $fs = new Filesystem(); + $fs->remove($this->cacheDir); + } } protected function configureRoutes(RouteCollectionBuilder $routes) @@ -100,7 +102,7 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load /** * {@inheritdoc} */ - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return [ KernelEvents::EXCEPTION => 'onKernelException', diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php index 539306fcea2b9..4d7a11ea62e0e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Kernel/MicroKernelTraitTest.php @@ -12,6 +12,9 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Kernel; use PHPUnit\Framework\TestCase; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; +use Symfony\Component\DependencyInjection\Loader\ClosureLoader; use Symfony\Component\HttpFoundation\Request; class MicroKernelTraitTest extends TestCase @@ -26,7 +29,7 @@ public function test() $this->assertEquals('halloween', $response->getContent()); $this->assertEquals('Have a great day!', $kernel->getContainer()->getParameter('halloween')); - $this->assertInstanceOf('stdClass', $kernel->getContainer()->get('halloween')); + $this->assertInstanceOf(\stdClass::class, $kernel->getContainer()->get('halloween')); } public function testAsEventSubscriber() @@ -39,4 +42,18 @@ public function testAsEventSubscriber() $this->assertSame('It\'s dangerous to go alone. Take this βš”', $response->getContent()); } + + public function testRoutingRouteLoaderTagIsAdded() + { + $frameworkExtension = $this->createMock(ExtensionInterface::class); + $frameworkExtension + ->expects($this->atLeastOnce()) + ->method('getAlias') + ->willReturn('framework'); + $container = new ContainerBuilder(); + $container->registerExtension($frameworkExtension); + $kernel = new ConcreteMicroKernel('test', false); + $kernel->registerContainerConfiguration(new ClosureLoader($container)); + $this->assertTrue($container->getDefinition('kernel')->hasTag('routing.route_loader')); + } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php index 7fb27a63ee720..404a239b51282 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/KernelBrowserTest.php @@ -12,10 +12,10 @@ namespace Symfony\Bundle\FrameworkBundle\Tests; use Symfony\Bundle\FrameworkBundle\KernelBrowser; -use Symfony\Bundle\FrameworkBundle\Tests\Functional\WebTestCase; +use Symfony\Bundle\FrameworkBundle\Tests\Functional\AbstractWebTestCase; use Symfony\Component\HttpFoundation\Response; -class KernelBrowserTest extends WebTestCase +class KernelBrowserTest extends AbstractWebTestCase { public function testRebootKernelBetweenRequests() { @@ -51,10 +51,20 @@ public function testEnableRebootKernel() $client->request('GET', '/'); } + public function testRequestAfterKernelShutdownAndPerformedRequest() + { + $this->expectNotToPerformAssertions(); + + $client = static::createClient(['test_case' => 'TestServiceContainer']); + $client->request('GET', '/'); + static::ensureKernelShutdown(); + $client->request('GET', '/'); + } + private function getKernelMock() { $mock = $this->getMockBuilder($this->getKernelClass()) - ->setMethods(['shutdown', 'boot', 'handle']) + ->setMethods(['shutdown', 'boot', 'handle', 'getContainer']) ->disableOriginalConstructor() ->getMock(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php index daed030f721a2..af4c3f51a2193 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/DelegatingLoaderTest.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\FrameworkBundle\Tests\Routing; use PHPUnit\Framework\TestCase; @@ -10,6 +19,7 @@ use Symfony\Component\Config\Loader\LoaderResolverInterface; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; +use Symfony\Component\Routing\RouteCompiler; class DelegatingLoaderTest extends TestCase { @@ -19,20 +29,16 @@ class DelegatingLoaderTest extends TestCase */ public function testConstructorApi() { - $controllerNameParser = $this->getMockBuilder(ControllerNameParser::class) - ->disableOriginalConstructor() - ->getMock(); + $controllerNameParser = $this->createMock(ControllerNameParser::class); new DelegatingLoader($controllerNameParser, new LoaderResolver()); $this->assertTrue(true, '__construct() takes a ControllerNameParser and LoaderResolverInterface respectively as its first and second argument.'); } public function testLoadDefaultOptions() { - $loaderResolver = $this->getMockBuilder(LoaderResolverInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $loaderResolver = $this->createMock(LoaderResolverInterface::class); - $loader = $this->getMockBuilder(LoaderInterface::class)->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loaderResolver->expects($this->once()) ->method('resolve') @@ -52,13 +58,13 @@ public function testLoadDefaultOptions() $this->assertCount(2, $loadedRouteCollection); $expected = [ - 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'compiler_class' => RouteCompiler::class, 'utf8' => false, ]; $this->assertSame($expected, $routeCollection->get('foo')->getOptions()); $expected = [ - 'compiler_class' => 'Symfony\Component\Routing\RouteCompiler', + 'compiler_class' => RouteCompiler::class, 'foo' => 123, 'utf8' => true, ]; @@ -71,20 +77,15 @@ public function testLoadDefaultOptions() */ public function testLoad() { - $controllerNameParser = $this->getMockBuilder(ControllerNameParser::class) - ->disableOriginalConstructor() - ->getMock(); - + $controllerNameParser = $this->createMock(ControllerNameParser::class); $controllerNameParser->expects($this->once()) ->method('parse') ->with('foo:bar:baz') ->willReturn('some_parsed::controller'); - $loaderResolver = $this->getMockBuilder(LoaderResolverInterface::class) - ->disableOriginalConstructor() - ->getMock(); + $loaderResolver = $this->createMock(LoaderResolverInterface::class); - $loader = $this->getMockBuilder(LoaderInterface::class)->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loaderResolver->expects($this->once()) ->method('resolve') diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Fixtures/with_condition.yaml b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Fixtures/with_condition.yaml new file mode 100644 index 0000000000000..c97edc1a42542 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/Fixtures/with_condition.yaml @@ -0,0 +1,3 @@ +foo: + path: /foo + condition: '%parameter.condition%' diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.php new file mode 100644 index 0000000000000..b0492efd89da4 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/LegacyRouteLoaderContainerTest.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\Bundle\FrameworkBundle\Tests\Routing; + +use PHPUnit\Framework\TestCase; +use Psr\Container\ContainerInterface; +use Symfony\Bundle\FrameworkBundle\Routing\LegacyRouteLoaderContainer; +use Symfony\Component\DependencyInjection\Container; + +/** + * @group legacy + */ +class LegacyRouteLoaderContainerTest extends TestCase +{ + /** + * @var ContainerInterface + */ + private $container; + + /** + * @var ContainerInterface + */ + private $serviceLocator; + + /** + * @var LegacyRouteLoaderContainer + */ + private $legacyRouteLoaderContainer; + + /** + * {@inheritdoc} + */ + protected function setUp(): void + { + $this->container = new Container(); + $this->container->set('foo', new \stdClass()); + + $this->serviceLocator = new Container(); + $this->serviceLocator->set('bar', new \stdClass()); + + $this->legacyRouteLoaderContainer = new LegacyRouteLoaderContainer($this->container, $this->serviceLocator); + } + + /** + * @expectedDeprecation Registering the service route loader "foo" without tagging it with the "routing.route_loader" tag is deprecated since Symfony 4.4 and will be required in Symfony 5.0. + */ + public function testGet() + { + $this->assertSame($this->container->get('foo'), $this->legacyRouteLoaderContainer->get('foo')); + $this->assertSame($this->serviceLocator->get('bar'), $this->legacyRouteLoaderContainer->get('bar')); + } + + public function testHas() + { + $this->assertTrue($this->legacyRouteLoaderContainer->has('foo')); + $this->assertTrue($this->legacyRouteLoaderContainer->has('bar')); + $this->assertFalse($this->legacyRouteLoaderContainer->has('ccc')); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php index 3b8efc2bfa697..3e6c5bf6c602e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Routing/RouterTest.php @@ -14,19 +14,25 @@ use PHPUnit\Framework\TestCase; use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Routing\Router; +use Symfony\Component\Config\FileLocator; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\Config\ResourceCheckerConfigCache; +use Symfony\Component\Config\ResourceCheckerConfigCacheFactory; use Symfony\Component\DependencyInjection\Config\ContainerParametersResource; +use Symfony\Component\DependencyInjection\Config\ContainerParametersResourceChecker; +use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException; +use Symfony\Component\DependencyInjection\Exception\RuntimeException; +use Symfony\Component\Routing\Loader\YamlFileLoader; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; class RouterTest extends TestCase { - /** - * @expectedException \LogicException - * @expectedExceptionMessage You should either pass a "Symfony\Component\DependencyInjection\ContainerInterface" instance or provide the $parameters argument of the "Symfony\Bundle\FrameworkBundle\Routing\Router::__construct" method - */ public function testConstructThrowsOnNonSymfonyNorPsr11Container() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('You should either pass a "Symfony\Component\DependencyInjection\ContainerInterface" instance or provide the $parameters argument of the "Symfony\Bundle\FrameworkBundle\Routing\Router::__construct" method'); new Router($this->createMock(ContainerInterface::class), 'foo'); } @@ -280,23 +286,21 @@ public function testPatternPlaceholdersWithSfContainer() $routes->add('foo', new Route('/before/%parameter.foo%/after/%%escaped%%')); $sc = $this->getServiceContainer($routes); - $sc->setParameter('parameter.foo', 'foo'); + $sc->setParameter('parameter.foo', 'foo-%%escaped%%'); $router = new Router($sc, 'foo'); $route = $router->getRouteCollection()->get('foo'); $this->assertEquals( - '/before/foo/after/%escaped%', + '/before/foo-%escaped%/after/%escaped%', $route->getPath() ); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Using "%env(FOO)%" is not allowed in routing configuration. - */ public function testEnvPlaceholders() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Using "%env(FOO)%" is not allowed in routing configuration.'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%env(FOO)%')); @@ -305,12 +309,10 @@ public function testEnvPlaceholders() $router->getRouteCollection(); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage Using "%env(FOO)%" is not allowed in routing configuration. - */ public function testEnvPlaceholdersWithSfContainer() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Using "%env(FOO)%" is not allowed in routing configuration.'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%env(FOO)%')); @@ -319,6 +321,22 @@ public function testEnvPlaceholdersWithSfContainer() $router->getRouteCollection(); } + public function testIndirectEnvPlaceholders() + { + $routes = new RouteCollection(); + + $routes->add('foo', new Route('/%foo%')); + + $router = new Router($container = $this->getServiceContainer($routes), 'foo'); + $container->setParameter('foo', 'foo-%bar%'); + $container->setParameter('bar', '%env(string:FOO)%'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Using "%env(string:FOO)%" is not allowed in routing configuration.'); + + $router->getRouteCollection(); + } + public function testHostPlaceholders() { $routes = new RouteCollection(); @@ -361,12 +379,10 @@ public function testHostPlaceholdersWithSfContainer() ); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\ParameterNotFoundException - * @expectedExceptionMessage You have requested a non-existent parameter "nope". - */ public function testExceptionOnNonExistentParameterWithSfContainer() { + $this->expectException(ParameterNotFoundException::class); + $this->expectExceptionMessage('You have requested a non-existent parameter "nope".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%nope%')); @@ -377,12 +393,10 @@ public function testExceptionOnNonExistentParameterWithSfContainer() $router->getRouteCollection()->get('foo'); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type object. - */ public function testExceptionOnNonStringParameter() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "object".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%object%')); @@ -394,12 +408,10 @@ public function testExceptionOnNonStringParameter() $router->getRouteCollection()->get('foo'); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\RuntimeException - * @expectedExceptionMessage The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type object. - */ public function testExceptionOnNonStringParameterWithSfContainer() { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('The container parameter "object", used in the route configuration value "/%object%", must be a string or numeric, but it is of type "object".'); $routes = new RouteCollection(); $routes->add('foo', new Route('/%object%')); @@ -497,11 +509,57 @@ public function getNonStringValues() } /** - * @return \Symfony\Component\DependencyInjection\Container + * @dataProvider getContainerParameterForRoute */ - private function getServiceContainer(RouteCollection $routes) + public function testCacheValidityWithContainerParameters($parameter) + { + $cacheDir = sys_get_temp_dir().\DIRECTORY_SEPARATOR.uniqid('router_', true); + + try { + $container = new Container(); + $container->set('routing.loader', new YamlFileLoader(new FileLocator(__DIR__.'/Fixtures'))); + + $container->setParameter('parameter.condition', $parameter); + + $router = new Router($container, 'with_condition.yaml', [ + 'debug' => true, + 'cache_dir' => $cacheDir, + ]); + + $resourceCheckers = [ + new ContainerParametersResourceChecker($container), + ]; + + $router->setConfigCacheFactory(new ResourceCheckerConfigCacheFactory($resourceCheckers)); + + $router->getMatcher(); // trigger cache build + + $cache = new ResourceCheckerConfigCache($cacheDir.\DIRECTORY_SEPARATOR.'url_matching_routes.php', $resourceCheckers); + + if (!$cache->isFresh()) { + $cache = new ResourceCheckerConfigCache($cacheDir.\DIRECTORY_SEPARATOR.'UrlMatcher.php', $resourceCheckers); + } + + $this->assertTrue($cache->isFresh()); + } finally { + if (is_dir($cacheDir)) { + array_map('unlink', glob($cacheDir.\DIRECTORY_SEPARATOR.'*')); + rmdir($cacheDir); + } + } + } + + public function getContainerParameterForRoute() + { + yield 'String' => ['"foo"']; + yield 'Integer' => [0]; + yield 'Boolean true' => [true]; + yield 'Boolean false' => [false]; + } + + private function getServiceContainer(RouteCollection $routes): Container { - $loader = $this->getMockBuilder('Symfony\Component\Config\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loader ->expects($this->any()) @@ -509,7 +567,7 @@ private function getServiceContainer(RouteCollection $routes) ->willReturn($routes) ; - $sc = $this->getMockBuilder('Symfony\\Component\\DependencyInjection\\Container')->setMethods(['get'])->getMock(); + $sc = $this->getMockBuilder(Container::class)->setMethods(['get'])->getMock(); $sc ->expects($this->once()) @@ -522,7 +580,7 @@ private function getServiceContainer(RouteCollection $routes) private function getPsr11ServiceContainer(RouteCollection $routes): ContainerInterface { - $loader = $this->getMockBuilder(LoaderInterface::class)->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loader ->expects($this->any()) @@ -530,7 +588,7 @@ private function getPsr11ServiceContainer(RouteCollection $routes): ContainerInt ->willReturn($routes) ; - $sc = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $sc = $this->createMock(ContainerInterface::class); $sc ->expects($this->once()) @@ -543,12 +601,12 @@ private function getPsr11ServiceContainer(RouteCollection $routes): ContainerInt private function getParameterBag(array $params = []): ContainerInterface { - $bag = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $bag = $this->createMock(ContainerInterface::class); $bag ->expects($this->any()) ->method('get') ->willReturnCallback(function ($key) use ($params) { - return isset($params[$key]) ? $params[$key] : null; + return $params[$key] ?? null; }) ; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.php new file mode 100644 index 0000000000000..0569f7de41c30 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/DotenvVaultTest.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\Bundle\FrameworkBundle\Tests\Secrets; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Secrets\DotenvVault; +use Symfony\Component\Dotenv\Dotenv; + +class DotenvVaultTest extends TestCase +{ + private $envFile; + + protected function setUp(): void + { + $this->envFile = sys_get_temp_dir().'/sf_secrets.env.test'; + @unlink($this->envFile); + } + + protected function tearDown(): void + { + @unlink($this->envFile); + } + + public function testGenerateKeys() + { + $vault = new DotenvVault($this->envFile); + + $this->assertFalse($vault->generateKeys()); + $this->assertSame('The dotenv vault doesn\'t encrypt secrets thus doesn\'t need keys.', $vault->getLastMessage()); + } + + public function testEncryptAndDecrypt() + { + $vault = new DotenvVault($this->envFile); + + $plain = "plain\ntext"; + + $vault->seal('foo', $plain); + + unset($_SERVER['foo'], $_ENV['foo']); + (new Dotenv(false))->load($this->envFile); + + $decrypted = $vault->reveal('foo'); + $this->assertSame($plain, $decrypted); + + $this->assertSame(['foo' => null], array_intersect_key($vault->list(), ['foo' => 123])); + $this->assertSame(['foo' => $plain], array_intersect_key($vault->list(true), ['foo' => 123])); + + $this->assertTrue($vault->remove('foo')); + $this->assertFalse($vault->remove('foo')); + + unset($_SERVER['foo'], $_ENV['foo']); + (new Dotenv(false))->load($this->envFile); + + $this->assertArrayNotHasKey('foo', $vault->list()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.php new file mode 100644 index 0000000000000..fff8f5af2216a --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Secrets/SodiumVaultTest.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\Bundle\FrameworkBundle\Tests\Secrets; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Secrets\SodiumVault; +use Symfony\Component\Filesystem\Filesystem; + +class SodiumVaultTest extends TestCase +{ + private $secretsDir; + + protected function setUp(): void + { + $this->secretsDir = sys_get_temp_dir().'/sf_secrets/test/'; + (new Filesystem())->remove($this->secretsDir); + } + + protected function tearDown(): void + { + (new Filesystem())->remove($this->secretsDir); + } + + public function testGenerateKeys() + { + $vault = new SodiumVault($this->secretsDir); + + $this->assertTrue($vault->generateKeys()); + $this->assertFileExists($this->secretsDir.'/test.encrypt.public.php'); + $this->assertFileExists($this->secretsDir.'/test.decrypt.private.php'); + + $encKey = file_get_contents($this->secretsDir.'/test.encrypt.public.php'); + $decKey = file_get_contents($this->secretsDir.'/test.decrypt.private.php'); + + $this->assertFalse($vault->generateKeys()); + $this->assertStringEqualsFile($this->secretsDir.'/test.encrypt.public.php', $encKey); + $this->assertStringEqualsFile($this->secretsDir.'/test.decrypt.private.php', $decKey); + + $this->assertTrue($vault->generateKeys(true)); + $this->assertStringNotEqualsFile($this->secretsDir.'/test.encrypt.public.php', $encKey); + $this->assertStringNotEqualsFile($this->secretsDir.'/test.decrypt.private.php', $decKey); + } + + public function testEncryptAndDecrypt() + { + $vault = new SodiumVault($this->secretsDir); + $vault->generateKeys(); + + $plain = "plain\ntext"; + + $vault->seal('foo', $plain); + + $decrypted = $vault->reveal('foo'); + $this->assertSame($plain, $decrypted); + + $this->assertSame(['foo' => null], $vault->list()); + $this->assertSame(['foo' => $plain], $vault->list(true)); + + $this->assertTrue($vault->remove('foo')); + $this->assertFalse($vault->remove('foo')); + + $this->assertSame([], $vault->list()); + } +} diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php index b7558544eca97..c26d90770f6bc 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/DelegatingEngineTest.php @@ -13,7 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Templating\DelegatingEngine; +use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Templating\EngineInterface; /** * @group legacy @@ -46,12 +48,10 @@ public function testGetExistingEngine() $this->assertSame($secondEngine, $delegatingEngine->getEngine('template.php')); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage No engine is able to work with the template "template.php" - */ public function testGetInvalidEngine() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('No engine is able to work with the template "template.php"'); $firstEngine = $this->getEngineMock('template.php', false); $secondEngine = $this->getEngineMock('template.php', false); $container = $this->getContainerMock([ @@ -84,12 +84,12 @@ public function testRenderResponseWithTemplatingEngine() $container = $this->getContainerMock(['engine' => $engine]); $delegatingEngine = new DelegatingEngine($container, ['engine']); - $this->assertInstanceOf('Symfony\Component\HttpFoundation\Response', $delegatingEngine->renderResponse('template.php', ['foo' => 'bar'])); + $this->assertInstanceOf(Response::class, $delegatingEngine->renderResponse('template.php', ['foo' => 'bar'])); } private function getEngineMock($template, $supports) { - $engine = $this->getMockBuilder('Symfony\Component\Templating\EngineInterface')->getMock(); + $engine = $this->createMock(EngineInterface::class); $engine->expects($this->once()) ->method('supports') @@ -101,7 +101,7 @@ private function getEngineMock($template, $supports) private function getFrameworkEngineMock($template, $supports) { - $engine = $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\EngineInterface')->getMock(); + $engine = $this->createMock(\Symfony\Bundle\FrameworkBundle\Templating\EngineInterface::class); $engine->expects($this->once()) ->method('supports') @@ -113,14 +113,10 @@ private function getFrameworkEngineMock($template, $supports) private function getContainerMock($services) { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = new ContainerBuilder(); - $i = 0; foreach ($services as $id => $service) { - $container->expects($this->at($i++)) - ->method('get') - ->with($id) - ->willReturn($service); + $container->set($id, $service); } return $container; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php index 4c3e57d88f200..2e8162baf04f8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/GlobalVariablesTest.php @@ -12,8 +12,11 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating; use Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables; +use Symfony\Bundle\FrameworkBundle\Tests\Fixtures\TokenInterface; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; +use Symfony\Component\Security\Core\User\UserInterface; /** * @group legacy @@ -23,7 +26,7 @@ class GlobalVariablesTest extends TestCase private $container; private $globals; - protected function setUp() + protected function setUp(): void { $this->container = new Container(); $this->globals = new GlobalVariables($this->container); @@ -36,14 +39,14 @@ public function testGetTokenNoTokenStorage() public function testGetTokenNoToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->container->set('security.token_storage', $tokenStorage); $this->assertNull($this->globals->getToken()); } public function testGetToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->container->set('security.token_storage', $tokenStorage); @@ -62,7 +65,7 @@ public function testGetUserNoTokenStorage() public function testGetUserNoToken() { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); $this->container->set('security.token_storage', $tokenStorage); $this->assertNull($this->globals->getUser()); } @@ -72,8 +75,8 @@ public function testGetUserNoToken() */ public function testGetUser($user, $expectedUser) { - $tokenStorage = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface')->getMock(); - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $tokenStorage = $this->createMock(TokenStorageInterface::class); + $token = $this->createMock(TokenInterface::class); $this->container->set('security.token_storage', $tokenStorage); @@ -92,9 +95,9 @@ public function testGetUser($user, $expectedUser) public function getUserProvider() { - $user = $this->getMockBuilder('Symfony\Component\Security\Core\User\UserInterface')->getMock(); + $user = $this->createMock(UserInterface::class); $std = new \stdClass(); - $token = $this->getMockBuilder('Symfony\Component\Security\Core\Authentication\Token\TokenInterface')->getMock(); + $token = $this->createMock(TokenInterface::class); return [ [$user, $user], diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php index 06e87f43f72ff..8861ff6eb9ff6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/AssetsHelperTest.php @@ -24,7 +24,7 @@ class AssetsHelperTest extends TestCase { private $helper; - protected function setUp() + protected function setUp(): void { $fooPackage = new Package(new StaticVersionStrategy('42', '%s?v=%s')); $barPackage = new Package(new StaticVersionStrategy('22', '%s?%s')); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php index 9835bc2a228ea..e6b8105806c71 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTemplateNameParser.php @@ -13,6 +13,7 @@ use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Templating\TemplateReference; +use Symfony\Component\Templating\TemplateReferenceInterface; class StubTemplateNameParser implements TemplateNameParserInterface { @@ -26,7 +27,7 @@ public function __construct($root, $rootTheme) $this->rootTheme = $rootTheme; } - public function parse($name) + public function parse($name): TemplateReferenceInterface { list($bundle, $controller, $template) = explode(':', $name, 3); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php index 402b3a886c74b..2f051f035e548 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Fixtures/StubTranslator.php @@ -15,7 +15,7 @@ class StubTranslator implements TranslatorInterface { - public function trans($id, array $parameters = [], $domain = null, $locale = null) + public function trans($id, array $parameters = [], $domain = null, $locale = null): string { return '[trans]'.strtr($id, $parameters).'[/trans]'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php index 729b01920f7d6..33ea9f380f902 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperDivLayoutTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper; use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser; use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator; @@ -30,13 +31,13 @@ class FormHelperDivLayoutTest extends AbstractDivLayoutTest */ protected $engine; - protected static $supportedFeatureSetVersion = 403; + protected static $supportedFeatureSetVersion = 404; protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported // by the default name parser in the Templating component - $reflClass = new \ReflectionClass('Symfony\Bundle\FrameworkBundle\FrameworkBundle'); + $reflClass = new \ReflectionClass(FrameworkBundle::class); $root = realpath(\dirname($reflClass->getFileName()).'/Resources/views'); $rootTheme = realpath(__DIR__.'/Resources'); $templateNameParser = new StubTemplateNameParser($root, $rootTheme); @@ -55,7 +56,7 @@ protected function getExtensions() ]); } - protected function tearDown() + protected function tearDown(): void { $this->engine = null; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php index 8e335788ea335..f985efce55dca 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/FormHelperTableLayoutTest.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\FrameworkBundle\Templating\Helper\TranslatorHelper; use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTemplateNameParser; use Symfony\Bundle\FrameworkBundle\Tests\Templating\Helper\Fixtures\StubTranslator; @@ -30,7 +31,7 @@ class FormHelperTableLayoutTest extends AbstractTableLayoutTest */ protected $engine; - protected static $supportedFeatureSetVersion = 403; + protected static $supportedFeatureSetVersion = 404; public function testStartTagHasNoActionAttributeWhenActionIsEmpty() { @@ -80,7 +81,7 @@ protected function getExtensions() { // should be moved to the Form component once absolute file paths are supported // by the default name parser in the Templating component - $reflClass = new \ReflectionClass('Symfony\Bundle\FrameworkBundle\FrameworkBundle'); + $reflClass = new \ReflectionClass(FrameworkBundle::class); $root = realpath(\dirname($reflClass->getFileName()).'/Resources/views'); $rootTheme = realpath(__DIR__.'/Resources'); $templateNameParser = new StubTemplateNameParser($root, $rootTheme); @@ -100,7 +101,7 @@ protected function getExtensions() ]); } - protected function tearDown() + protected function tearDown(): void { $this->engine = null; diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php index d29b5c0ff47b6..cddb14e5f9df9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/RequestHelperTest.php @@ -23,7 +23,7 @@ class RequestHelperTest extends TestCase { protected $requestStack; - protected function setUp() + protected function setUp(): void { $this->requestStack = new RequestStack(); $request = new Request(); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php index 1b53a7213f025..235028ee00fc2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/Resources/Parent/form_widget_simple.html.php @@ -1,2 +1,2 @@ - + block($form, 'widget_attributes'); ?> value="" rel="theme" /> diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php index c9521e8e54074..0ee9930efddf2 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/SessionHelperTest.php @@ -25,7 +25,7 @@ class SessionHelperTest extends TestCase { protected $requestStack; - protected function setUp() + protected function setUp(): void { $request = new Request(); @@ -39,7 +39,7 @@ protected function setUp() $this->requestStack->push($request); } - protected function tearDown() + protected function tearDown(): void { $this->requestStack = null; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php index f5030b4e79fc1..074916ed75156 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Helper/StopwatchHelperTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\FrameworkBundle\Templating\Helper\StopwatchHelper; +use Symfony\Component\Stopwatch\Stopwatch; /** * @group legacy @@ -21,7 +22,7 @@ class StopwatchHelperTest extends TestCase { public function testDevEnvironment() { - $stopwatch = $this->getMockBuilder('Symfony\Component\Stopwatch\Stopwatch')->getMock(); + $stopwatch = $this->createMock(Stopwatch::class); $stopwatch->expects($this->once()) ->method('start') ->with('foo'); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php index 393539952d1b2..5e0cea2cd12c9 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/Loader/TemplateLocatorTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Templating\Loader\TemplateLocator; use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\Config\FileLocator; /** * @group legacy @@ -72,7 +73,7 @@ public function testThrowsExceptionWhenTemplateNotFound() $locator->locate($template); $this->fail('->locate() should throw an exception when the file is not found.'); } catch (\InvalidArgumentException $e) { - $this->assertContains( + $this->assertStringContainsString( $errorMessage, $e->getMessage(), 'TemplateLocator exception should propagate the FileLocator exception message' @@ -80,11 +81,9 @@ public function testThrowsExceptionWhenTemplateNotFound() } } - /** - * @expectedException \InvalidArgumentException - */ public function testThrowsAnExceptionWhenTemplateIsNotATemplateReferenceInterface() { + $this->expectException(\InvalidArgumentException::class); $locator = new TemplateLocator($this->getFileLocator()); $locator->locate('template'); } @@ -92,7 +91,7 @@ public function testThrowsAnExceptionWhenTemplateIsNotATemplateReferenceInterfac protected function getFileLocator() { return $this - ->getMockBuilder('Symfony\Component\Config\FileLocator') + ->getMockBuilder(FileLocator::class) ->setMethods(['locate']) ->setConstructorArgs(['/path/to/fallback']) ->getMock() diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php index 47f3f360aa747..8c39fb08279d6 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/PhpEngineTest.php @@ -19,6 +19,7 @@ use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; use Symfony\Component\HttpFoundation\Session\Storage\MockArraySessionStorage; +use Symfony\Component\Templating\Loader\Loader; use Symfony\Component\Templating\TemplateNameParser; /** @@ -29,7 +30,7 @@ class PhpEngineTest extends TestCase public function testEvaluateAddsAppGlobal() { $container = $this->getContainer(); - $loader = $this->getMockForAbstractClass('Symfony\Component\Templating\Loader\Loader'); + $loader = $this->getMockForAbstractClass(Loader::class); $engine = new PhpEngine(new TemplateNameParser(), $container, $loader, $app = new GlobalVariables($container)); $globals = $engine->getGlobals(); $this->assertSame($app, $globals['app']); @@ -38,7 +39,7 @@ public function testEvaluateAddsAppGlobal() public function testEvaluateWithoutAvailableRequest() { $container = new Container(); - $loader = $this->getMockForAbstractClass('Symfony\Component\Templating\Loader\Loader'); + $loader = $this->getMockForAbstractClass(Loader::class); $engine = new PhpEngine(new TemplateNameParser(), $container, $loader, new GlobalVariables($container)); $this->assertFalse($container->has('request_stack')); @@ -46,13 +47,11 @@ public function testEvaluateWithoutAvailableRequest() $this->assertEmpty($globals['app']->getRequest()); } - /** - * @expectedException \InvalidArgumentException - */ public function testGetInvalidHelper() { + $this->expectException(\InvalidArgumentException::class); $container = $this->getContainer(); - $loader = $this->getMockForAbstractClass('Symfony\Component\Templating\Loader\Loader'); + $loader = $this->getMockForAbstractClass(Loader::class); $engine = new PhpEngine(new TemplateNameParser(), $container, $loader); $engine->get('non-existing-helper'); @@ -60,10 +59,8 @@ public function tes 10000 tGetInvalidHelper() /** * Creates a Container with a Session-containing Request service. - * - * @return Container */ - protected function getContainer() + protected function getContainer(): Container { $container = new Container(); $session = new Session(new MockArraySessionStorage()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php index 305be175910b8..58e671ddf358b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateFilenameParserTest.php @@ -22,12 +22,12 @@ class TemplateFilenameParserTest extends TestCase { protected $parser; - protected function setUp() + protected function setUp(): void { $this->parser = new TemplateFilenameParser(); } - protected function tearDown() + protected function tearDown(): void { $this->parser = null; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php index 49136769f2da4..4fe8aa3a4367e 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TemplateNameParserTest.php @@ -14,6 +14,7 @@ use Symfony\Bundle\FrameworkBundle\Templating\TemplateNameParser; use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Templating\TemplateReference as BaseTemplateReference; /** @@ -23,9 +24,9 @@ class TemplateNameParserTest extends TestCase { protected $parser; - protected function setUp() + protected function setUp(): void { - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\KernelInterface')->getMock(); + $kernel = $this->createMock(KernelInterface::class); $kernel ->expects($this->any()) ->method('getBundle') @@ -40,7 +41,7 @@ protected function setUp() $this->parser = new TemplateNameParser($kernel); } - protected function tearDown() + protected function tearDown(): void { $this->parser = null; } @@ -77,11 +78,9 @@ public function parseProvider() ]; } - /** - * @expectedException \InvalidArgumentException - */ public function testParseValidNameWithNotFoundBundle() { + $this->expectException(\InvalidArgumentException::class); $this->parser->parse('BarBundle:Post:index.html.php'); } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php index 4bdb0ccda565f..2db6d689530ff 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Templating/TimedPhpEngineTest.php @@ -15,6 +15,11 @@ use Symfony\Bundle\FrameworkBundle\Templating\TimedPhpEngine; use Symfony\Bundle\FrameworkBundle\Tests\TestCase; use Symfony\Component\DependencyInjection\Container; +use Symfony\Component\Stopwatch\Stopwatch; +use Symfony\Component\Templating\Loader\Loader; +use Symfony\Component\Templating\Storage\StringStorage; +use Symfony\Component\Templating\TemplateNameParserInterface; +use Symfony\Component\Templating\TemplateReferenceInterface; /** * @group legacy @@ -23,40 +28,26 @@ class TimedPhpEngineTest extends TestCase { public function testThatRenderLogsTime() { - $container = $this->getContainer(); + $container = $this->createMock(Container::class); $templateNameParser = $this->getTemplateNameParser(); $globalVariables = $this->getGlobalVariables(); - $loader = $this->getLoader($this->getStorage()); + $loader = $this->getLoader(new StringStorage('foo')); - $stopwatch = $this->getStopwatch(); - $stopwatchEvent = $this->getStopwatchEvent(); - - $stopwatch->expects($this->once()) - ->method('start') - ->with('template.php (index.php)', 'template') - ->willReturn($stopwatchEvent); - - $stopwatchEvent->expects($this->once())->method('stop'); + $stopwatch = new Stopwatch(); $engine = new TimedPhpEngine($templateNameParser, $container, $loader, $stopwatch, $globalVariables); $engine->render('index.php'); - } - /** - * @return Container - */ - private function getContainer() - { - return $this->getMockBuilder('Symfony\Component\DependencyInjection\Container')->getMock(); + $sections = $stopwatch->getSections(); + + $this->assertCount(1, $sections); + $this->assertCount(1, reset($sections)->getEvents()); } - /** - * @return \Symfony\Component\Templating\TemplateNameParserInterface - */ - private function getTemplateNameParser() + private function getTemplateNameParser(): TemplateNameParserInterface { - $templateReference = $this->getMockBuilder('Symfony\Component\Templating\TemplateReferenceInterface')->getMock(); - $templateNameParser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); + $templateReference = $this->createMock(TemplateReferenceInterface::class); + $templateNameParser = $this->createMock(TemplateNameParserInterface::class); $templateNameParser->expects($this->any()) ->method('parse') ->willReturn($templateReference); @@ -64,56 +55,18 @@ private function getTemplateNameParser() return $templateNameParser; } - /** - * @return GlobalVariables - */ - private function getGlobalVariables() - { - return $this->getMockBuilder('Symfony\Bundle\FrameworkBundle\Templating\GlobalVariables') - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @return \Symfony\Component\Templating\Storage\StringStorage - */ - private function getStorage() + private function getGlobalVariables(): GlobalVariables { - return $this->getMockBuilder('Symfony\Component\Templating\Storage\StringStorage') - ->disableOriginalConstructor() - ->getMockForAbstractClass(); + return $this->createMock(GlobalVariables::class); } - /** - * @param \Symfony\Component\Templating\Storage\StringStorage $storage - * - * @return \Symfony\Component\Templating\Loader\Loader - */ - private function getLoader($storage) + private function getLoader(StringStorage $storage): Loader { - $loader = $this->getMockForAbstractClass('Symfony\Component\Templating\Loader\Loader'); + $loader = $this->getMockForAbstractClass(Loader::class); $loader->expects($this->once()) ->method('load') ->willReturn($storage); return $loader; } - - /** - * @return \Symfony\Component\Stopwatch\StopwatchEvent - */ - private function getStopwatchEvent() - { - return $this->getMockBuilder('Symfony\Component\Stopwatch\StopwatchEvent') - ->disableOriginalConstructor() - ->getMock(); - } - - /** - * @return \Symfony\Component\Stopwatch\Stopwatch - */ - private function getStopwatch() - { - return $this->getMockBuilder('Symfony\Component\Stopwatch\Stopwatch')->getMock(); - } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php index c77e5a6f2def7..ff88b34007069 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Test/WebTestCaseTest.php @@ -70,7 +70,7 @@ public function testAssertResponseRedirectsWithLocationAndStatusCode() { $this->getResponseTester(new Response('', 302, ['Location' => 'https://example.com/']))->assertResponseRedirects('https://example.com/', 302); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('is redirected and has header "Location" with value "https://example.com/" and status code is 301.'); + $this->expectExceptionMessageMatches('#(:?\( )?is redirected and has header "Location" with value "https://example\.com/" (:?\) )?and status code is 301\.#'); $this->getResponseTester(new Response('', 302))->assertResponseRedirects('https://example.com/', 301); } @@ -183,7 +183,7 @@ public function testAssertSelectorTextNotContains() { $this->getCrawlerTester(new Crawler('

Foo'))->assertSelectorTextNotContains('body > h1', 'Bar'); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('matches selector "body > h1" and does not have a node matching selector "body > h1" with content containing "Foo".'); + $this->expectExceptionMessage('matches selector "body > h1" and the text "Foo" of the node matching selector "body > h1" does not contain "Foo".'); $this->getCrawlerTester(new Crawler('

Foo'))->assertSelectorTextNotContains('body > h1', 'Foo'); } @@ -199,7 +199,7 @@ public function testAssertPageTitleContains() { $this->getCrawlerTester(new Crawler('Foobar'))->assertPageTitleContains('Foo'); $this->expectException(AssertionFailedError::class); - $this->expectExceptionMessage('matches selector "title" and has a node matching selector "title" with content containing "Bar".'); + $this->expectExceptionMessage('matches selector "title" and the text "Foo" of the node matching selector "title" contains "Bar".'); $this->getCrawlerTester(new Crawler('<html><head><title>Foo'))->assertPageTitleContains('Bar'); } @@ -274,13 +274,14 @@ private function getRequestTester(): WebTestCase private function getTester(KernelBrowser $client): WebTestCase { - return new class($client) extends WebTestCase { - use WebTestAssertionsTrait; - - public function __construct(KernelBrowser $client) - { - self::getClient($client); + $tester = new class() extends WebTestCase { + use WebTestAssertionsTrait { + getClient as public; } }; + + $tester::getClient($client); + + return $tester; } } diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php index 61f600a03f265..c05b7093c7538 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Translation/TranslatorTest.php @@ -12,25 +12,28 @@ namespace Symfony\Bundle\FrameworkBundle\Tests\Translation; use PHPUnit\Framework\TestCase; -use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\Translation\Translator; use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\FileExistenceResource; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Translation\Exception\InvalidArgumentException; use Symfony\Component\Translation\Formatter\MessageFormatter; +use Symfony\Component\Translation\Loader\LoaderInterface; +use Symfony\Component\Translation\Loader\YamlFileLoader; use Symfony\Component\Translation\MessageCatalogue; class TranslatorTest extends TestCase { protected $tmpDir; - protected function setUp() + protected function setUp(): void { $this->tmpDir = sys_get_temp_dir().'/sf_translation'; $this->deleteTmpDir(); } - protected function tearDown() + protected function tearDown(): void { $this->deleteTmpDir(); } @@ -93,7 +96,7 @@ public function testTransWithCaching() $this->assertEquals('foobarbax (sr@latin)', $translator->trans('foobarbax')); // do it another time as the cache is primed now - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loader->expects($this->never())->method('load'); $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir]); @@ -123,7 +126,7 @@ public function testTransChoiceWithCaching() $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); // do it another time as the cache is primed now - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loader->expects($this->never())->method('load'); $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir]); @@ -134,21 +137,19 @@ public function testTransChoiceWithCaching() $this->assertEquals('other choice 1 (PT-BR)', $translator->transChoice('other choice', 1)); } - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Invalid "invalid locale" locale. - */ public function testTransWithCachingWithInvalidLocale() { - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); - $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir], 'loader', '\Symfony\Bundle\FrameworkBundle\Tests\Translation\TranslatorWithInvalidLocale'); + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid "invalid locale" locale.'); + $loader = $this->createMock(LoaderInterface::class); + $translator = $this->getTranslator($loader, ['cache_dir' => $this->tmpDir], 'loader', TranslatorWithInvalidLocale::class); $translator->trans('foo'); } public function testLoadResourcesWithoutCaching() { - $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $loader = new YamlFileLoader(); $resourceFiles = [ 'fr' => [ __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', @@ -163,21 +164,19 @@ public function testLoadResourcesWithoutCaching() public function testGetDefaultLocale() { - $container = $this->getMockBuilder(ContainerInterface::class)->getMock(); + $container = $this->createMock(\Psr\Container\ContainerInterface::class); $translator = new Translator($container, new MessageFormatter(), 'en'); $this->assertSame('en', $translator->getLocale()); } - /** - * @expectedException \Symfony\Component\Translation\Exception\InvalidArgumentException - * @expectedExceptionMessage The Translator does not support the following options: 'foo' - */ public function testInvalidOptions() { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The Translator does not support the following options: \'foo\''); + $container = $this->createMock(ContainerInterface::class); - (new Translator($container, new MessageFormatter(), 'en', [], ['foo' => 'bar'])); + new Translator($container, new MessageFormatter(), 'en', [], ['foo' => 'bar']); } /** @dataProvider getDebugModeAndCacheDirCombinations */ @@ -185,19 +184,20 @@ public function testResourceFilesOptionLoadsBeforeOtherAddedResources($debug, $e { $someCatalogue = $this->getCatalogue('some_locale', []); - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); - - $loader->expects($this->at(0)) - ->method('load') - /* The "messages.some_locale.loader" is passed via the resource_file option and shall be loaded first */ - ->with('messages.some_locale.loader', 'some_locale', 'messages') - ->willReturn($someCatalogue); + $loader = $this->createMock(LoaderInterface::class); - $loader->expects($this->at(1)) + $loader->expects($this->exactly(2)) ->method('load') - /* This resource is added by an addResource() call and shall be loaded after the resource_files */ - ->with('second_resource.some_locale.loader', 'some_locale', 'messages') - ->willReturn($someCatalogue); + ->withConsecutive( + /* The "messages.some_locale.loader" is passed via the resource_file option and shall be loaded first */ + ['messages.some_locale.loader', 'some_locale', 'messages'], + /* This resource is added by an addResource() call and shall be loaded after the resource_files */ + ['second_resource.some_locale.loader', 'some_locale', 'messages'] + ) + ->willReturnOnConsecutiveCalls( + $someCatalogue, + $someCatalogue + ); $options = [ 'resource_files' => ['some_locale' => ['messages.some_locale.loader']], @@ -227,7 +227,7 @@ public function getDebugModeAndCacheDirCombinations() public function testCatalogResourcesAreAddedForScannedDirectories() { - $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $loader = new YamlFileLoader(); $resourceFiles = [ 'fr' => [ __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', @@ -248,6 +248,45 @@ public function testCatalogResourcesAreAddedForScannedDirectories() $this->assertEquals(new FileExistenceResource('/tmp/I/sure/hope/this/does/not/exist'), $resources[2]); } + public function testCachedCatalogueIsReDumpedWhenScannedDirectoriesChange() + { + /** @var Translator $translator */ + $translator = $this->getTranslator(new YamlFileLoader(), [ + 'cache_dir' => $this->tmpDir, + 'resource_files' => [ + 'fr' => [ + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + ], + ], + 'cache_vary' => [ + 'scanned_directories' => [ + '/Fixtures/Resources/translations/', + ], + ], + ], 'yml'); + + // Cached catalogue is dumped + $this->assertSame('rΓ©pertoire', $translator->trans('folder', [], 'messages', 'fr')); + + $translator = $this->getTranslator(new YamlFileLoader(), [ + 'cache_dir' => $this->tmpDir, + 'resource_files' => [ + 'fr' => [ + __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', + __DIR__.'/../Fixtures/Resources/translations2/ccc.fr.yml', + ], + ], + 'cache_vary' => [ + 'scanned_directories' => [ + '/Fixtures/Resources/translations/', + '/Fixtures/Resources/translations2/', + ], + ], + ], 'yml'); + + $this->assertSame('bar', $translator->trans('foo', [], 'ccc', 'fr')); + } + protected function getCatalogue($locale, $messages, $resources = []) { $catalogue = new MessageCatalogue($locale); @@ -263,57 +302,35 @@ protected function getCatalogue($locale, $messages, $resources = []) protected function getLoader() { - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); - $loader - ->expects($this->at(0)) - ->method('load') - ->willReturn($this->getCatalogue('fr', [ - 'foo' => 'foo (FR)', - ])) - ; - $loader - ->expects($this->at(1)) - ->method('load') - ->willReturn($this->getCatalogue('en', [ - 'foo' => 'foo (EN)', - 'bar' => 'bar (EN)', - 'choice' => '{0} choice 0 (EN)|{1} choice 1 (EN)|]1,Inf] choice inf (EN)', - ])) - ; - $loader - ->expects($this->at(2)) - ->method('load') - ->willReturn($this->getCatalogue('es', [ - 'foobar' => 'foobar (ES)', - ])) - ; - $loader - ->expects($this->at(3)) - ->method('load') - ->willReturn($this->getCatalogue('pt-PT', [ - 'foobarfoo' => 'foobarfoo (PT-PT)', - ])) - ; - $loader - ->expects($this->at(4)) - ->method('load') - ->willReturn($this->getCatalogue('pt_BR', [ - 'other choice' => '{0} other choice 0 (PT-BR)|{1} other choice 1 (PT-BR)|]1,Inf] other choice inf (PT-BR)', - ])) - ; - $loader - ->expects($this->at(5)) - ->method('load') - ->willReturn($this->getCatalogue('fr.UTF-8', [ - 'foobarbaz' => 'foobarbaz (fr.UTF-8)', - ])) - ; + $loader = $this->createMock(LoaderInterface::class); $loader - ->expects($this->at(6)) + ->expects($this->exactly(7)) ->method('load') - ->willReturn($this->getCatalogue('sr@latin', [ - 'foobarbax' => 'foobarbax (sr@latin)', - ])) + ->willReturnOnConsecutiveCalls( + $this->getCatalogue('fr', [ + 'foo' => 'foo (FR)', + ]), + $this->getCatalogue('en', [ + 'foo' => 'foo (EN)', + 'bar' => 'bar (EN)', + 'choice' => '{0} choice 0 (EN)|{1} choice 1 (EN)|]1,Inf] choice inf (EN)', + ]), + $this->getCatalogue('es', [ + 'foobar' => 'foobar (ES)', + ]), + $this->getCatalogue('pt-PT', [ + 'foobarfoo' => 'foobarfoo (PT-PT)', + ]), + $this->getCatalogue('pt_BR', [ + 'other choice' => '{0} other choice 0 (PT-BR)|{1} other choice 1 (PT-BR)|]1,Inf] other choice inf (PT-BR)', + ]), + $this->getCatalogue('fr.UTF-8', [ + 'foobarbaz' => 'foobarbaz (fr.UTF-8)', + ]), + $this->getCatalogue('sr@latin', [ + 'foobarbax' => 'foobarbax (sr@latin)', + ]) + ) ; return $loader; @@ -321,7 +338,7 @@ protected function getLoader() protected function getContainer($loader) { - $container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerInterface')->getMock(); + $container = $this->createMock(ContainerInterface::class); $container ->expects($this->any()) ->method('get') @@ -331,7 +348,7 @@ protected function getContainer($loader) return $container; } - public function getTranslator($loader, $options = [], $loaderFomat = 'loader', $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $defaultLocale = 'en') + public function getTranslator($loader, $options = [], $loaderFomat = 'loader', $translatorClass = Translator::class, $defaultLocale = 'en') { $translator = $this->createTranslator($loader, $options, $translatorClass, $loaderFomat, $defaultLocale); @@ -350,7 +367,7 @@ public function getTranslator($loader, $options = [], $loaderFomat = 'loader', $ public function testWarmup() { - $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $loader = new YamlFileLoader(); $resourceFiles = [ 'fr' => [ __DIR__.'/../Fixtures/Resources/translations/messages.fr.yml', @@ -362,7 +379,7 @@ public function testWarmup() $translator->setFallbackLocales(['fr']); $translator->warmup($this->tmpDir); - $loader = $this->getMockBuilder('Symfony\Component\Translation\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(LoaderInterface::class); $loader ->expects($this->never()) ->method('load'); @@ -375,7 +392,7 @@ public function testWarmup() public function testLoadingTranslationFilesWithDotsInMessageDomain() { - $loader = new \Symfony\Component\Translation\Loader\YamlFileLoader(); + $loader = new YamlFileLoader(); $resourceFiles = [ 'en' => [ __DIR__.'/../Fixtures/Resources/translations/domain.with.dots.en.yml', @@ -388,7 +405,7 @@ public function testLoadingTranslationFilesWithDotsInMessageDomain() $this->assertEquals('It works!', $translator->trans('message', [], 'domain.with.dots')); } - private function createTranslator($loader, $options, $translatorClass = '\Symfony\Bundle\FrameworkBundle\Translation\Translator', $loaderFomat = 'loader', $defaultLocale = 'en') + private function createTranslator($loader, $options, $translatorClass = Translator::class, $loaderFomat = 'loader', $defaultLocale = 'en') { if (null === $defaultLocale) { return new $translatorClass( @@ -414,7 +431,7 @@ class TranslatorWithInvalidLocale extends Translator /** * {@inheritdoc} */ - public function getLocale() + public function getLocale(): string { return 'invalid locale'; } diff --git a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php index 1b2dc7e3fa60d..cd5444034ba7c 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php +++ b/src/Symfony/Bundle/FrameworkBundle/Translation/Translator.php @@ -20,8 +20,6 @@ use Symfony\Component\Translation\Translator as BaseTranslator; /** - * Translator. - * * @author Fabien Potencier <fabien@symfony.com> */ class Translator extends BaseTranslator implements WarmableInterface @@ -34,6 +32,7 @@ class Translator extends BaseTranslator implements WarmableInterface 'debug' => false, 'resource_files' => [], 'scanned_directories' => [], + 'cache_vary' => [], ]; /** @@ -61,15 +60,10 @@ class Translator extends BaseTranslator implements WarmableInterface * * Available options: * - * * cache_dir: The cache directory (or null to disable caching) - * * debug: Whether to enable debugging or not (false by default) + * * cache_dir: The cache directory (or null to disable caching) + * * debug: Whether to enable debugging or not (false by default) * * resource_files: List of translation resources available grouped by locale. - * - * @param ContainerInterface $container A ContainerInterface instance - * @param MessageFormatterInterface $formatter The message formatter - * @param string $defaultLocale - * @param array $loaderIds An array of loader Ids - * @param array $options An array of options + * * cache_vary: An array of data that is serialized to generate the cached catalogue name. * * @throws InvalidArgumentException */ @@ -88,7 +82,7 @@ public function __construct(ContainerInterface $container, MessageFormatterInter $this->resourceFiles = $this->options['resource_files']; $this->scannedDirectories = $this->options['scanned_directories']; - parent::__construct($defaultLocale, $formatter, $this->options['cache_dir'], $this->options['debug']); + parent::__construct($defaultLocale, $formatter, $this->options['cache_dir'], $this->options['debug'], $this->options['cache_vary']); } /** @@ -129,7 +123,10 @@ protected function initializeCatalogue($locale) parent::initializeCatalogue($locale); } - protected function doLoadCatalogue($locale): void + /** + * @internal + */ + protected function doLoadCatalogue(string $locale): void { parent::doLoadCatalogue($locale); @@ -145,7 +142,7 @@ protected function initialize() $this->addResourceFiles(); } foreach ($this->resources as $key => $params) { - list($format, $resource, $locale, $domain) = $params; + [$format, $resource, $locale, $domain] = $params; parent::addResource($format, $resource, $locale, $domain); } $this->resources = []; diff --git a/src/Symfony/Bundle/FrameworkBundle/composer.json b/src/Symfony/Bundle/FrameworkBundle/composer.json index 3d7e99a5ccc70..bf3ffcac6fe0f 100644 --- a/src/Symfony/Bundle/FrameworkBundle/composer.json +++ b/src/Symfony/Bundle/FrameworkBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/framework-bundle", "type": "symfony-bundle", - "description": "Symfony FrameworkBundle", + "description": "Provides a tight integration between Symfony components and the Symfony full-stack framework", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,72 +16,82 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "ext-xml": "*", "symfony/cache": "^4.4|^5.0", - "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/error-renderer": "^4.4|^5.0", - "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^4.4|^5.0", + "symfony/config": "^4.4.11|~5.0.11|^5.1.3", + "symfony/dependency-injection": "^4.4.38|^5.0.1", + "symfony/error-handler": "^4.4.1|^5.0.1", + "symfony/http-foundation": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.16", "symfony/filesystem": "^3.4|^4.0|^5.0", "symfony/finder": "^3.4|^4.0|^5.0", - "symfony/routing": "^4.3|^5.0" + "symfony/routing": "^4.4.12|^5.1.4" }, "require-dev": { - "doctrine/cache": "~1.0", - "fig/link-util": "^1.0", + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "^1.0|^2.0", + "doctrine/persistence": "^1.3|^2|^3", + "paragonie/sodium_compat": "^1.8", "symfony/asset": "^3.4|^4.0|^5.0", "symfony/browser-kit": "^4.3|^5.0", - "symfony/console": "^4.3|^5.0", + "symfony/console": "^4.4.42|^5.4.9", "symfony/css-selector": "^3.4|^4.0|^5.0", - "symfony/dom-crawler": "^4.3|^5.0", + "symfony/dom-crawler": "^4.4.30|^5.3.7", + "symfony/dotenv": "^4.3.6|^5.0", "symfony/polyfill-intl-icu": "~1.0", - "symfony/form": "^4.3|^5.0", + "symfony/form": "^4.3.5|^5.0", "symfony/expression-language": "^3.4|^4.0|^5.0", - "symfony/http-client": "^4.3|^5.0", - "symfony/mailer": "^4.3|^5.0", - "symfony/messenger": "^4.3|^5.0", - "symfony/mime": "^4.3|^5.0", + "symfony/http-client": "^4.4|^5.0", + "symfony/lock": "^4.4|^5.0", + "symfony/mailer": "^4.4|^5.0", + "symfony/messenger": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0", "symfony/process": "^3.4|^4.0|^5.0", + "symfony/security-core": "^3.4|^4.4|^5.2", "symfony/security-csrf": "^3.4|^4.0|^5.0", "symfony/security-http": "^3.4|^4.0|^5.0", - "symfony/serializer": "^4.3|^5.0", + "symfony/serializer": "^4.4|^5.0", "symfony/stopwatch": "^3.4|^4.0|^5.0", - "symfony/translation": "^4.3|^5.0", + "symfony/translation": "^4.4|^5.0", "symfony/templating": "^3.4|^4.0|^5.0", - "symfony/twig-bundle": "^3.4|^4.0|^5.0", - "symfony/validator": "^4.1|^5.0", - "symfony/var-dumper": "^4.3|^5.0", - "symfony/workflow": "^4.3|^5.0", + "symfony/twig-bundle": "^4.4|^5.0", + "symfony/validator": "^4.4|^5.0", + "symfony/workflow": "^4.3.6|^5.0", "symfony/yaml": "^3.4|^4.0|^5.0", "symfony/property-info": "^3.4|^4.0|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/web-link": "^3.4|^4.0|^5.0", - "doctrine/annotations": "~1.0", - "phpdocumentor/reflection-docblock": "^3.0|^4.0", - "twig/twig": "~1.34|~2.4" + "symfony/web-link": "^4.4|^5.0", + "phpdocumentor/reflection-docblock": "^3.0|^4.0|^5.0", + "twig/twig": "^1.43|^2.13|^3.0.4" }, "conflict": { - "phpdocumentor/reflection-docblock": "<3.0", - "phpdocumentor/type-resolver": "<0.2.1", + "doctrine/persistence": "<1.3", + "phpdocumentor/reflection-docblock": "<3.0|>=3.2.0,<3.2.2", + "phpdocumentor/type-resolver": "<0.3.0|1.3.*", "phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0", "symfony/asset": "<3.4", "symfony/browser-kit": "<4.3", - "symfony/console": "<4.3", - "symfony/dotenv": "<4.2", + "symfony/console": "<4.4.21", + "symfony/dotenv": "<4.3.6", "symfony/dom-crawler": "<4.3", - "symfony/form": "<4.3", + "symfony/http-client": "<4.4", + "symfony/form": "<4.3.5", "symfony/lock": "<4.4", - "symfony/messenger": "<4.3", + "symfony/mailer": "<4.4", + "symfony/messenger": "<4.4", + "symfony/mime": "<4.4", "symfony/property-info": "<3.4", - "symfony/serializer": "<4.2", + "symfony/security-bundle": "<4.4", + "symfony/serializer": "<4.4", "symfony/stopwatch": "<3.4", - "symfony/translation": "<4.3", + "symfony/translation": "<4.4", "symfony/twig-bridge": "<4.1.1", - "symfony/validator": "<4.1", - "symfony/workflow": "<4.3" + "symfony/twig-bundle": "<4.4", + "symfony/validator": "<4.4", + "symfony/web-profiler-bundle": "<4.4", + "symfony/workflow": "<4.3.6" }, "suggest": { "ext-apcu": "For best performance of the system caches", @@ -99,10 +109,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/SecurityBundle/.gitattributes b/src/Symfony/Bundle/SecurityBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md index 1a6f7169a2cc8..f46d5231851c4 100644 --- a/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/SecurityBundle/CHANGELOG.md @@ -2,8 +2,13 @@ CHANGELOG ========= 4.4.0 +----- -* Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories. + * Added `anonymous: lazy` mode to firewalls to make them (not) start the session as late as possible + * Added `migrate_from` option to encoders configuration. + * Added new `argon2id` encoder, undeprecated the `bcrypt` and `argon2i` ones (using `auto` is still recommended by default.) + * Deprecated the usage of "query_string" without a "search_dn" and a "search_password" config key in Ldap factories. + * Marked the `SecurityDataCollector` class as `@final`. 4.3.0 ----- @@ -13,7 +18,6 @@ CHANGELOG option is deprecated and will be disabled in Symfony 5.0. This affects to cookies with dashes in their names. For example, starting from Symfony 5.0, the `my-cookie` name will delete `my-cookie` (with a dash) instead of `my_cookie` (with an underscore). - * Deprecated configuring encoders using `argon2i` as algorithm, use `auto` instead 4.2.0 ----- @@ -35,6 +39,7 @@ CHANGELOG 4.1.0 ----- + * The `switch_user.stateless` firewall option is deprecated, use the `stateless` option instead. * The `logout_on_user_change` firewall option is deprecated. * deprecated `SecurityUserValueResolver`, use `Symfony\Component\Security\Http\Controller\UserValueResolver` instead. diff --git a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php index 938b28dcb637b..130cdfbb11e7f 100644 --- a/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php +++ b/src/Symfony/Bundle/SecurityBundle/CacheWarmer/ExpressionCacheWarmer.php @@ -37,7 +37,7 @@ public function isOptional() public function warmUp($cacheDir) { foreach ($this->expressions as $expression) { - $this->expressionLanguage->parse($expression, ['token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver']); + $this->expressionLanguage->parse($expression, ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']); } } } diff --git a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php index 84ad3e4c8b92e..b13ebbf33294c 100644 --- a/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php +++ b/src/Symfony/Bundle/SecurityBundle/Command/UserPasswordEncoderCommand.php @@ -52,7 +52,7 @@ public function __construct(EncoderFactoryInterface $encoderFactory, array $user protected function configure() { $this - ->setDescription('Encodes a password.') + ->setDescription('Encode a password.') ->addArgument('password', InputArgument::OPTIONAL, 'The plain password to encode.') ->addArgument('user-class', InputArgument::OPTIONAL, 'The User entity class path associated with the encoder used to encode the password.') ->addOption('empty-salt', null, InputOption::VALUE_NONE, 'Do not generate a salt or let the encoder generate one.') @@ -82,16 +82,16 @@ protected function configure() Pass the full user class path as the second argument to encode passwords for your own entities: - <info>php %command.full_name% --no-interaction [password] App\Entity\User</info> + <info>php %command.full_name% --no-interaction [password] 'App\Entity\User'</info> Executing the command interactively allows you to generate a random salt for encoding the password: - <info>php %command.full_name% [password] App\Entity\User</info> + <info>php %command.full_name% [password] 'App\Entity\User'</info> In case your encoder doesn't require a salt, add the <comment>empty-salt</comment> option: - <info>php %command.full_name% --empty-salt [password] App\Entity\User</info> + <info>php %command.full_name% --empty-salt [password] 'App\Entity\User'</info> EOF ) @@ -101,7 +101,7 @@ protected function configure() /** * {@inheritdoc} */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); $errorIo = $output instanceof ConsoleOutputInterface ? new SymfonyStyle($input, $output->getErrorOutput()) : $io; @@ -134,7 +134,7 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($input->isInteractive() && !$emptySalt) { $emptySalt = true; - $errorIo->note('The command will take care of generating a salt for you. Be aware that some encoders advise to let them generate their own salt. If you\'re using one of those encoders, please answer \'no\' to the question below. '.PHP_EOL.'Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'); + $errorIo->note('The command will take care of generating a salt for you. Be aware that some encoders advise to let them generate their own salt. If you\'re using one of those encoders, please answer \'no\' to the question below. '.\PHP_EOL.'Provide the \'empty-salt\' option in order to let the encoder handle the generation itself.'); if ($errorIo->confirm('Confirm salt generation ?')) { $salt = $this->generateSalt(); @@ -162,6 +162,8 @@ protected function execute(InputInterface $input, OutputInterface $output) } $errorIo->success('Password encoding succeeded'); + + return 0; } /** @@ -180,12 +182,12 @@ private function createPasswordQuestion(): Question })->setHidden(true)->setMaxAttempts(20); } - private function generateSalt() + private function generateSalt(): string { return base64_encode(random_bytes(30)); } - private function getUserClass(InputInterface $input, SymfonyStyle $io) + private function getUserClass(InputInterface $input, SymfonyStyle $io): string { if (null !== $userClass = $input->getArgument('user-class')) { return $userClass; diff --git a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php index 0d122efe7fd81..6efe388aab8b0 100644 --- a/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php +++ b/src/Symfony/Bundle/SecurityBundle/DataCollector/SecurityDataCollector.php @@ -17,6 +17,7 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\DataCollector; use Symfony\Component\HttpKernel\DataCollector\LateDataCollectorInterface; +use Symfony\Component\Security\Core\Authentication\Token\AnonymousToken; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; use Symfony\Component\Security\Core\Authentication\Token\SwitchUserToken; use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface; @@ -33,6 +34,8 @@ /** * @author Fabien Potencier <fabien@symfony.com> + * + * @final since Symfony 4.4 */ class SecurityDataCollector extends DataCollector implements LateDataCollectorInterface { @@ -57,8 +60,10 @@ public function __construct(TokenStorageInterface $tokenStorage = null, RoleHier /** * {@inheritdoc} + * + * @param \Throwable|null $exception */ - public function collect(Request $request, Response $response, \Exception $exception = null) + public function collect(Request $request, Response $response/* , \Throwable $exception = null */) { if (null === $this->tokenStorage) { $this->data = [ @@ -127,7 +132,7 @@ public function collect(Request $request, Response $response, \Exception $except $logoutUrl = null; try { - if (null !== $this->logoutUrlGenerator) { + if (null !== $this->logoutUrlGenerator && !$token instanceof AnonymousToken) { $logoutUrl = $this->logoutUrlGenerator->getLogoutPath(); } } catch (\Exception $e) { @@ -259,7 +264,7 @@ public function getUser() /** * Gets the roles of the user. * - * @return array The roles + * @return array|Data */ public function getRoles() { @@ -269,7 +274,7 @@ public function getRoles() /** * Gets the inherited roles of the user. * - * @return array The inherited roles + * @return array|Data */ public function getInheritedRoles() { @@ -297,16 +302,25 @@ public function isAuthenticated() return $this->data['authenticated']; } + /** + * @return bool + */ public function isImpersonated() { return $this->data['impersonated']; } + /** + * @return string|null + */ public function getImpersonatorUser() { return $this->data['impersonator_user']; } + /** + * @return string|null + */ public function getImpersonationExitPath() { return $this->data['impersonation_exit_path']; @@ -315,7 +329,7 @@ public function getImpersonationExitPath() /** * Get the class name of the security token. * - * @return string The token + * @return string|Data|null The token */ public function getTokenClass() { @@ -325,7 +339,7 @@ public function getTokenClass() /** * Get the full security token class as Data object. * - * @return Data + * @return Data|null */ public function getToken() { @@ -335,7 +349,7 @@ public function getToken() /** * Get the logout URL. * - * @return string The logout URL + * @return string|null The logout URL */ public function getLogoutUrl() { @@ -345,7 +359,7 @@ public function getLogoutUrl() /** * Returns the FQCN of the security voters enabled in the application. * - * @return string[] + * @return string[]|Data */ public function getVoters() { @@ -365,7 +379,7 @@ public function getVoterStrategy() /** * Returns the log of the security decisions made by the access decision manager. * - * @return array + * @return array|Data */ public function getAccessDecisionLog() { @@ -375,13 +389,16 @@ public function getAccessDecisionLog() /** * Returns the configuration of the current firewall context. * - * @return array + * @return array|Data|null */ public function getFirewall() { return $this->data['firewall']; } + /** + * @return array|Data + */ public function getListeners() { return $this->data['listeners']; diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php index 62be170ddc1d6..e7f9df1221e69 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableFirewallListener.php @@ -12,7 +12,10 @@ namespace Symfony\Bundle\SecurityBundle\Debug; use Symfony\Bundle\SecurityBundle\EventListener\FirewallListener; +use Symfony\Bundle\SecurityBundle\Security\FirewallContext; +use Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext; use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Http\Firewall\AbstractListener; /** * Firewall collecting called listeners. @@ -21,7 +24,7 @@ */ final class TraceableFirewallListener extends FirewallListener { - private $wrappedListeners; + private $wrappedListeners = []; public function getWrappedListeners() { @@ -30,14 +33,47 @@ public function getWrappedListeners() protected function callListeners(RequestEvent $event, iterable $listeners) { + $wrappedListeners = []; + $wrappedLazyListeners = []; + foreach ($listeners as $listener) { - $wrappedListener = new WrappedListener($listener); - $wrappedListener($event); - $this->wrappedListeners[] = $wrappedListener->getInfo(); + if ($listener instanceof LazyFirewallContext) { + \Closure::bind(function () use (&$wrappedLazyListeners, &$wrappedListeners) { + $listeners = []; + foreach ($this->listeners as $listener) { + if ($listener instanceof AbstractListener) { + $listener = new WrappedLazyListener($listener); + $listeners[] = $listener; + $wrappedLazyListeners[] = $listener; + } else { + $listeners[] = function (RequestEvent $event) use ($listener, &$wrappedListeners) { + $wrappedListener = new WrappedListener($listener); + $wrappedListener($event); + $wrappedListeners[] = $wrappedListener->getInfo(); + }; + } + } + $this->listeners = $listeners; + }, $listener, FirewallContext::class)(); + + $listener($event); + } else { + $wrappedListener = $listener instanceof AbstractListener ? new WrappedLazyListener($listener) : new WrappedListener($listener); + $wrappedListener($event); + $wrappedListeners[] = $wrappedListener->getInfo(); + } if ($event->hasResponse()) { break; } } + + if ($wrappedLazyListeners) { + foreach ($wrappedLazyListeners as $lazyListener) { + $this->wrappedListeners[] = $lazyListener->getInfo(); + } + } + + $this->wrappedListeners = array_merge($this->wrappedListeners, $wrappedListeners); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php new file mode 100644 index 0000000000000..b758be6242660 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/TraceableListenerTrait.php @@ -0,0 +1,52 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Component\Security\Http\Firewall\LegacyListenerTrait; +use Symfony\Component\VarDumper\Caster\ClassStub; + +/** + * @author Robin Chalas <robin.chalas@gmail.com> + * + * @internal + */ +trait TraceableListenerTrait +{ + use LegacyListenerTrait; + + private $response; + private $listener; + private $time; + private $stub; + + /** + * Proxies all method calls to the original listener. + */ + public function __call(string $method, array $arguments) + { + return $this->listener->{$method}(...$arguments); + } + + public function getWrappedListener() + { + return $this->listener; + } + + public function getInfo(): array + { + return [ + 'response' => $this->response, + 'time' => $this->time, + 'stub' => $this->stub ?? $this->stub = ClassStub::wrapCallable($this->listener), + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php new file mode 100644 index 0000000000000..eca63b4d4a2fc --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedLazyListener.php @@ -0,0 +1,62 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Debug; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Exception\LazyResponseException; +use Symfony\Component\Security\Http\Firewall\AbstractListener; +use Symfony\Component\Security\Http\Firewall\ListenerInterface; + +/** + * Wraps a lazy security listener. + * + * @author Robin Chalas <robin.chalas@gmail.com> + * + * @internal + */ +final class WrappedLazyListener extends AbstractListener implements ListenerInterface +{ + use TraceableListenerTrait; + + public function __construct(AbstractListener $listener) + { + $this->listener = $listener; + } + + public function supports(Request $request): ?bool + { + return $this->listener->supports($request); + } + + /** + * {@inheritdoc} + */ + public function authenticate(RequestEvent $event) + { + $startTime = microtime(true); + + try { + $ret = $this->listener->authenticate($event); + } catch (LazyResponseException $e) { + $this->response = $e->getResponse(); + + throw $e; + } finally { + $this->time = microtime(true) - $startTime; + } + + $this->response = $event->getResponse(); + + return $ret; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php index 42735a1025e1c..8404c73b0f577 100644 --- a/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php +++ b/src/Symfony/Bundle/SecurityBundle/Debug/WrappedListener.php @@ -12,9 +12,8 @@ namespace Symfony\Bundle\SecurityBundle\Debug; use Symfony\Component\HttpKernel\Event\RequestEvent; -use Symfony\Component\Security\Http\Firewall\LegacyListenerTrait; +use Symfony\Component\Security\Http\Firewall\AbstractListener; use Symfony\Component\Security\Http\Firewall\ListenerInterface; -use Symfony\Component\VarDumper\Caster\ClassStub; /** * Wraps a security listener for calls record. @@ -25,13 +24,7 @@ */ final class WrappedListener implements ListenerInterface { - use LegacyListenerTrait; - - private $response; - private $listener; - private $time; - private $stub; - private static $hasVarDumper; + use TraceableListenerTrait; /** * @param callable $listener @@ -50,53 +43,10 @@ public function __invoke(RequestEvent $event) if (\is_callable($this->listener)) { ($this->listener)($event); } else { - @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, implement "__invoke()" instead.', \get_class($this)), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($this->listener), AbstractListener::class), \E_USER_DEPRECATED); $this->listener->handle($event); } $this->time = microtime(true) - $startTime; $this->response = $event->getResponse(); } - - /** - * Proxies all method calls to the original listener. - */ - public function __call($method, $arguments) - { - return $this->listener->{$method}(...$arguments); - } - - public function getWrappedListener() - { - return $this->listener; - } - - public function getInfo(): array - { - if (null !== $this->stub) { - // no-op - } elseif (self::$hasVarDumper ?? self::$hasVarDumper = class_exists(ClassStub::class)) { - $this->stub = ClassStub::wrapCallable($this->listener); - } elseif (\is_array($this->listener)) { - $this->stub = (\is_object($this->listener[0]) ? \get_class($this->listener[0]) : $this->listener[0]).'::'.$this->listener[1]; - } elseif ($this->listener instanceof \Closure) { - $r = new \ReflectionFunction($this->listener); - if (false !== strpos($r->name, '{closure}')) { - $this->stub = 'closure'; - } elseif ($class = $r->getClosureScopeClass()) { - $this->stub = $class->name.'::'.$r->name; - } else { - $this->stub = $r->name; - } - } elseif (\is_string($this->listener)) { - $this->stub = $this->listener; - } else { - $this->stub = \get_class($this->listener).'::__invoke'; - } - - return [ - 'response' => $this->response, - 'time' => $this->time, - 'stub' => $this->stub, - ]; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php index 898402d35939c..0393e6c616b79 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddExpressionLanguageProvidersPass.php @@ -33,5 +33,9 @@ public function process(ContainerBuilder $container) $definition->addMethodCall('registerProvider', [new Reference($id)]); } } + + if (!$container->hasDefinition('cache.system')) { + $container->removeDefinition('cache.security_expression_language'); + } } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php index 87103c37a30c0..c3c7b86ab2f72 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSecurityVotersPass.php @@ -52,7 +52,7 @@ public function process(ContainerBuilder $container) $class = $container->getParameterBag()->resolveValue($definition->getClass()); if (!is_a($class, VoterInterface::class, true)) { - throw new LogicException(sprintf('%s must implement the %s when used as a voter.', $class, VoterInterface::class)); + throw new LogicException(sprintf('"%s" must implement the "%s" when used as a voter.', $class, VoterInterface::class)); } if ($debug) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php index b1e653a10975f..461a5ad66d081 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/AddSessionDomainConstraintPass.php @@ -31,7 +31,7 @@ public function process(ContainerBuilder $container) } $sessionOptions = $container->getParameter('session.storage.options'); - $domainRegexp = empty($sessionOptions['cookie_domain']) ? '%s' : sprintf('(?:%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.'))); + $domainRegexp = empty($sessionOptions['cookie_domain']) ? '%%s' : sprintf('(?:%%%%s|(?:.+\.)?%s)', preg_quote(trim($sessionOptions['cookie_domain'], '.'))); if ('auto' === ($sessionOptions['cookie_secure'] ?? null)) { $secureDomainRegexp = sprintf('{^https://%s$}i', $domainRegexp); diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php new file mode 100644 index 0000000000000..6aa50f4196c8b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Compiler/RegisterTokenUsageTrackingPass.php @@ -0,0 +1,56 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler; + +use Monolog\Processor\ProcessorInterface; +use Symfony\Component\DependencyInjection\Argument\BoundArgument; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface; + +/** + * Injects the session tracker enabler in "security.context_listener" + binds "security.untracked_token_storage" to ProcessorInterface instances. + * + * @author Nicolas Grekas <p@tchwork.com> + * + * @internal + */ +class RegisterTokenUsageTrackingPass implements CompilerPassInterface +{ + /** + * {@inheritdoc} + */ + public function process(ContainerBuilder $container) + { + if (!$container->has('security.untracked_token_storage')) { + return; + } + + $processorAutoconfiguration = $container->registerForAutoconfiguration(ProcessorInterface::class); + $processorAutoconfiguration->setBindings($processorAutoconfiguration->getBindings() + [ + TokenStorageInterface::class => new BoundArgument(new Reference('security.untracked_token_storage'), false), + ]); + + if (!$container->has('session')) { + $container->setAlias('security.token_storage', 'security.untracked_token_storage')->setPublic(true); + $container->getDefinition('security.untracked_token_storage')->addTag('kernel.reset', ['method' => 'reset']); + } elseif ($container->hasDefinition('security.context_listener')) { + $tokenStorageClass = $container->getParameterBag()->resolveValue($container->findDefinition('security.token_storage')->getClass()); + + if (method_exists($tokenStorageClass, 'enableUsageTracking')) { + $container->getDefinition('security.context_listener') + ->setArgument(6, [new Reference('security.token_storage'), 'enableUsageTracking']); + } + } + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php index 1e1e97ccb5b3f..2f8714aaf7221 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/MainConfiguration.php @@ -47,24 +47,6 @@ public function getConfigTreeBuilder() $rootNode = $tb->getRootNode(); $rootNode - ->beforeNormalization() - ->ifTrue(function ($v) { - if (!isset($v['access_decision_manager'])) { - return true; - } - - if (!isset($v['access_decision_manager']['strategy']) && !isset($v['access_decision_manager']['service'])) { - return true; - } - - return false; - }) - ->then(function ($v) { - $v['access_decision_manager']['strategy'] = AccessDecisionManager::STRATEGY_AFFIRMATIVE; - - return $v; - }) - ->end() ->children() ->scalarNode('access_denied_url')->defaultNull()->example('/foo/error403')->end() ->enumNode('session_fixation_strategy') @@ -226,9 +208,9 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->beforeNormalization() ->ifArray()->then(function ($v) { foreach ($v as $originalName => $cookieConfig) { - if (false !== strpos($originalName, '-')) { + if (str_contains($originalName, '-')) { $normalizedName = str_replace('-', '_', $originalName); - @trigger_error(sprintf('Normalization of cookie names is deprecated since Symfony 4.3. Starting from Symfony 5.0, the "%s" cookie configured in "logout.delete_cookies" will delete the "%s" cookie instead of the "%s" cookie.', $originalName, $originalName, $normalizedName), E_USER_DEPRECATED); + @trigger_error(sprintf('Normalization of cookie names is deprecated since Symfony 4.3. Starting from Symfony 5.0, the "%s" cookie configured in "logout.delete_cookies" will delete the "%s" cookie instead of the "%s" cookie.', $originalName, $originalName, $normalizedName), \E_USER_DEPRECATED); // normalize cookie names manually for BC reasons. Remove it in Symfony 5.0. $v[$normalizedName] = $cookieConfig; @@ -244,6 +226,8 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto ->children() ->scalarNode('path')->defaultNull()->end() ->scalarNode('domain')->defaultNull()->end() + ->scalarNode('secure')->defaultFalse()->end() + ->scalarNode('samesite')->defaultNull()->end() ->end() ->end() ->end() @@ -255,12 +239,6 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto 10000 ->end() ->end() ->end() - ->arrayNode('anonymous') - ->canBeUnset() - ->children() - ->scalarNode('secret')->defaultNull()->end() - ->end() - ->end() ->arrayNode('switch_user') ->canBeUnset() ->children() @@ -308,7 +286,7 @@ private function addFirewallsSection(ArrayNodeDefinition $rootNode, array $facto continue; } - if (false !== strpos($firewall[$k]['check_path'], '/') && !preg_match('#'.$firewall['pattern'].'#', $firewall[$k]['check_path'])) { + if (str_contains($firewall[$k]['check_path'], '/') && !preg_match('#'.$firewall['pattern'].'#', $firewall[$k]['check_path'])) { throw new \LogicException(sprintf('The check_path "%s" for login method "%s" is not matched by the firewall pattern "%s".', $firewall[$k]['check_path'], $k, $firewall['pattern'])); } } @@ -399,7 +377,17 @@ private function addEncodersSection(ArrayNodeDefinition $rootNode) ->performNoDeepMerging() ->beforeNormalization()->ifString()->then(function ($v) { return ['algorithm' => $v]; })->end() ->children() - ->scalarNode('algorithm')->cannotBeEmpty()->end() + ->scalarNode('algorithm') + ->cannotBeEmpty() + ->validate() + ->ifTrue(function ($v) { return !\is_string($v); }) + ->thenInvalid('You must provide a string value.') + ->end() + ->end() + ->arrayNode('migrate_from') + ->prototype('scalar')->end() + ->beforeNormalization()->castToArray()->end() + ->end() ->scalarNode('hash_algorithm')->info('Name of hashing algorithm for PBKDF2 (i.e. sha256, sha512, etc..) See hash_algos() for a list of supported algorithms.')->defaultValue('sha512')->end() ->scalarNode('key_length')->defaultValue(40)->end() ->booleanNode('ignore_case')->defaultFalse()->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 00bb451e0ef02..79ef873ec2ad2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -89,7 +89,7 @@ public function addConfiguration(NodeDefinition $node) } } - final public function addOption($name, $default = null) + final public function addOption(string $name, $default = null) { $this->options[$name] = $default; } @@ -98,10 +98,9 @@ final public function addOption($name, $default = null) * Subclasses must return the id of a service which implements the * AuthenticationProviderInterface. * - * @param ContainerBuilder $container - * @param string $id The unique id of the firewall - * @param array $config The options array for this listener - * @param string $userProviderId The id of the user provider + * @param string $id The unique id of the firewall + * @param array $config The options array for this listener + * @param string $userProviderId The id of the user provider * * @return string never null, the id of the authentication provider */ @@ -131,9 +130,9 @@ abstract protected function getListenerId(); * @param ContainerBuilder $container * @param string $id * @param array $config - * @param string $defaultEntryPointId + * @param string|null $defaultEntryPointId * - * @return string the entry point id + * @return string|null the entry point id */ protected function createEntryPoint($container, $id, $config, $defaultEntryPointId) { diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php new file mode 100644 index 0000000000000..eb3c930afe379 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AnonymousFactory.php @@ -0,0 +1,68 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory; + +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\ChildDefinition; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Parameter; + +/** + * @author Wouter de Jong <wouter@wouterj.nl> + */ +class AnonymousFactory implements SecurityFactoryInterface +{ + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + { + if (null === $config['secret']) { + $config['secret'] = new Parameter('container.build_hash'); + } + + $listenerId = 'security.authentication.listener.anonymous.'.$id; + $container + ->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.anonymous')) + ->replaceArgument(1, $config['secret']) + ; + + $providerId = 'security.authentication.provider.anonymous.'.$id; + $container + ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.anonymous')) + ->replaceArgument(0, $config['secret']) + ; + + return [$providerId, $listenerId, $defaultEntryPoint]; + } + + public function getPosition() + { + return 'anonymous'; + } + + public function getKey() + { + return 'anonymous'; + } + + public function addConfiguration(NodeDefinition $builder) + { + $builder + ->beforeNormalization() + ->ifTrue(function ($v) { return 'lazy' === $v; }) + ->then(function ($v) { return ['lazy' => true]; }) + ->end() + ->children() + ->booleanNode('lazy')->defaultFalse()->end() + ->scalarNode('secret')->defaultNull()->end() + ->end() + ; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php index 0a3f92f402ede..59ba5e45f8f90 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php @@ -34,13 +34,13 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config, ->replaceArgument(2, $id) ->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(4, $config['dn_string']) - ->replaceArgument(5, $config['search_dn']) - ->replaceArgument(6, $config['search_password']) + ->replaceArgument(6, $config['search_dn']) + ->replaceArgument(7, $config['search_password']) ; if (!empty($config['query_string'])) { if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw in Symfony 5.0.', E_USER_DEPRECATED); + @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); } $definition->addMethodCall('setQueryString', [$config['query_string']]); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php index 8384c42da7e66..45a457aef8a62 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/GuardAuthenticationFactory.php @@ -92,7 +92,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, return [$providerId, $listenerId, $entryPointId]; } - private function determineEntryPoint($defaultEntryPointId, array $config) + private function determineEntryPoint(?string $defaultEntryPointId, array $config): string { if ($defaultEntryPointId) { // explode if they've configured the entry_point, but there is already one @@ -115,6 +115,6 @@ private function determineEntryPoint($defaultEntryPointId, array $config) } // we have multiple entry points - we must ask them to configure one - throw new \LogicException(sprintf('Because you have multiple guard authenticators, you need to set the "guard.entry_point" key to one of your authenticators (%s)', implode(', ', $authenticatorIds))); + throw new \LogicException(sprintf('Because you have multiple guard authenticators, you need to set the "guard.entry_point" key to one of your authenticators (%s).', implode(', ', $authenticatorIds))); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php index 309d114fab3f3..8980c88140834 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -35,8 +35,8 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, ->replaceArgument(2, $id) ->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(4, $config['dn_string']) - ->replaceArgument(5, $config['search_dn']) - ->replaceArgument(6, $config['search_password']) + ->replaceArgument(6, $config['search_dn']) + ->replaceArgument(7, $config['search_password']) ; // entry point @@ -44,7 +44,7 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, if (!empty($config['query_string'])) { if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw in Symfony 5.0.', E_USER_DEPRECATED); + @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); } $definition->addMethodCall('setQueryString', [$config['query_string']]); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php index 09b99dd3a2ac1..5b1be3dbf9b6a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php @@ -36,13 +36,13 @@ protected function createAuthProvider(ContainerBuilder $container, $id, $config, ->replaceArgument(2, $id) ->replaceArgument(3, new Reference($config['service'])) ->replaceArgument(4, $config['dn_string']) - ->replaceArgument(5, $config['search_dn']) - ->replaceArgument(6, $config['search_password']) + ->replaceArgument(6, $config['search_dn']) + ->replaceArgument(7, $config['search_password']) ; if (!empty($config['query_string'])) { if ('' === $config['search_dn'] || '' === $config['search_password']) { - @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw in Symfony 5.0.', E_USER_DEPRECATED); + @trigger_error('Using the "query_string" config without using a "search_dn" and a "search_password" is deprecated since Symfony 4.4 and will throw an exception in Symfony 5.0.', \E_USER_DEPRECATED); } $definition->addMethodCall('setQueryString', [$config['query_string']]); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index f7500f05e3b9f..153028165c987 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -69,7 +69,12 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, } // remember-me options - $rememberMeServices->replaceArgument(3, array_intersect_key($config, $this->options)); + $mergedOptions = array_intersect_key($config, $this->options); + if ('auto' === $mergedOptions['secure']) { + $mergedOptions['secure'] = null; + } + + $rememberMeServices->replaceArgument(3, $mergedOptions); // attach to remember-me aware listeners $userProviders = []; @@ -83,7 +88,11 @@ public function create(ContainerBuilder $container, $id, $config, $userProvider, throw new \RuntimeException('Each "security.remember_me_aware" tag must have a provider attribute.'); } - $userProviders[] = new Reference($attribute['provider']); + // context listeners don't need a provider + if ('none' !== $attribute['provider']) { + $userProviders[] = new Reference($attribute['provider']); + } + $container ->getDefinition($serviceId) ->addMethodCall('setRememberMeServices', [new Reference($rememberMeServicesId)]) @@ -137,16 +146,18 @@ public function addConfiguration(NodeDefinition $node) ->end() ->prototype('scalar')->end() ->end() - ->scalarNode('catch_exceptions')->defaultTrue()->end() + ->booleanNode('catch_exceptions')->defaultTrue()->end() ; foreach ($this->options as $name => $value) { if ('secure' === $name) { $builder->enumNode($name)->values([true, false, 'auto'])->defaultValue('auto' === $value ? null : $value); } elseif ('samesite' === $name) { - $builder->enumNode($name)->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT])->defaultValue($value); + $builder->enumNode($name)->values([null, Cookie::SAMESITE_LAX, Cookie::SAMESITE_STRICT, Cookie::SAMESITE_NONE])->defaultValue($value); } elseif (\is_bool($value)) { $builder->booleanNode($name)->defaultValue($value); + } elseif (\is_int($value)) { + $builder->integerNode($name)->defaultValue($value); } else { $builder->scalarNode($name)->defaultValue($value); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index 028e885246f61..06bae0c7b91ee 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -24,18 +24,17 @@ interface SecurityFactoryInterface /** * Configures the container services required to use the authentication listener. * - * @param ContainerBuilder $container - * @param string $id The unique id of the firewall - * @param array $config The options array for the listener - * @param string $userProvider The service id of the user provider - * @param string $defaultEntryPoint + * @param string $id The unique id of the firewall + * @param array $config The options array for the listener + * @param string $userProviderId The service id of the user provider + * @param string|null $defaultEntryPointId * * @return array containing three values: * - the provider id * - the listener id * - the entry point id */ - public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint); + public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId); /** * Defines the position at which the provider is called. diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php index 9ffd624a96d92..4fa128273db5b 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimpleFormFactory.php @@ -30,7 +30,7 @@ public function __construct(bool $triggerDeprecation = true) $this->addOption('authenticator', null); if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), \E_USER_DEPRECATED); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php index 04c5ce16aed02..01a8521c34b17 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SimplePreAuthenticationFactory.php @@ -26,7 +26,7 @@ class SimplePreAuthenticationFactory implements SecurityFactoryInterface public function __construct(bool $triggerDeprecation = true) { if ($triggerDeprecation) { - @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.2, use Guard instead.', __CLASS__), \E_USER_DEPRECATED); } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php index 33e59bfc70e74..5007b740363f4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php @@ -48,11 +48,13 @@ public function getKey() public function addConfiguration(NodeDefinition $node) { $node + ->fixXmlConfig('extra_field') + ->fixXmlConfig('default_role') ->children() ->scalarNode('service')->isRequired()->cannotBeEmpty()->defaultValue('ldap')->end() ->scalarNode('base_dn')->isRequired()->cannotBeEmpty()->end() - ->scalarNode('search_dn')->end() - ->scalarNode('search_password')->end() + ->scalarNode('search_dn')->defaultNull()->end() + ->scalarNode('search_password')->defaultNull()->end() ->arrayNode('extra_fields') ->prototype('scalar')->end() ->end() diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 4d5e6f4ae4edf..1996249a72070 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -25,11 +25,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; -use Symfony\Component\DependencyInjection\Parameter; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\HttpKernel\DependencyInjection\Extension; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\VoterInterface; -use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; use Symfony\Component\Security\Core\User\UserProviderInterface; @@ -48,7 +47,7 @@ class SecurityExtension extends Extension implements PrependExtensionInterface private $requestMatchers = []; private $expressions = []; private $contextListeners = []; - private $listenerPositions = ['pre_auth', 'form', 'http', 'remember_me']; + private $listenerPositions = ['pre_auth', 'form', 'http', 'remember_me', 'anonymous']; private $factories = []; private $userProviderFactories = []; private $statelessFirewallKeys = []; @@ -117,7 +116,7 @@ public function load(array $configs, ContainerBuilder $container) $loader->load('security_debug.xml'); } - if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) { $container->removeDefinition('security.expression_language'); $container->removeDefinition('security.access.expression_voter'); } @@ -132,7 +131,7 @@ public function load(array $configs, ContainerBuilder $container) } else { $container ->getDefinition('security.access.decision_manager') - ->addArgument($config['access_decision_manager']['strategy']) + ->addArgument($config['access_decision_manager']['strategy'] ?? AccessDecisionManager::STRATEGY_AFFIRMATIVE) ->addArgument($config['access_decision_manager']['allow_if_all_abstain']) ->addArgument($config['access_decision_manager']['allow_if_equal_granted_denied']); } @@ -176,7 +175,7 @@ private function createRoleHierarchy(array $config, ContainerBuilder $container) $container->removeDefinition('security.access.simple_role_voter'); } - private function createAuthorization($config, ContainerBuilder $container) + private function createAuthorization(array $config, ContainerBuilder $container) { foreach ($config['access_control'] as $access) { $matcher = $this->createRequestMatcher( @@ -193,6 +192,12 @@ private function createAuthorization($config, ContainerBuilder $container) $attributes[] = $this->createExpression($container, $access['allow_if']); } + $emptyAccess = 0 === \count(array_filter($access)); + + if ($emptyAccess) { + throw new InvalidConfigurationException('One or more access control items are empty. Did you accidentally add lines only containing a "-" under "security.access_control"?'); + } + $container->getDefinition('security.access_map') ->addMethodCall('add', [$matcher, $attributes, $access['requires_channel']]); } @@ -206,7 +211,7 @@ private function createAuthorization($config, ContainerBuilder $container) } } - private function createFirewalls($config, ContainerBuilder $container) + private function createFirewalls(array $config, ContainerBuilder $container) { if (!isset($config['firewalls'])) { return; @@ -241,10 +246,11 @@ private function createFirewalls($config, ContainerBuilder $container) $configId = 'security.firewall.map.config.'.$name; - list($matcher, $listeners, $exceptionListener, $logoutListener) = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); + [$matcher, $listeners, $exceptionListener, $logoutListener] = $this->createFirewall($container, $name, $firewall, $authenticationProviders, $providerIds, $configId); $contextId = 'security.firewall.map.context.'.$name; - $context = $container->setDefinition($contextId, new ChildDefinition('security.firewall.context')); + $context = new ChildDefinition($firewall['stateless'] || empty($firewall['anonymous']['lazy']) ? 'security.firewall.context' : 'security.firewall.lazy_context'); + $context = $container->setDefinition($contextId, $context); $context ->replaceArgument(0, new IteratorArgument($listeners)) ->replaceArgument(1, $exceptionListener) @@ -273,7 +279,7 @@ private function createFirewalls($config, ContainerBuilder $container) } } - private function createFirewall(ContainerBuilder $container, $id, $firewall, &$authenticationProviders, $providerIds, $configId) + private function createFirewall(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, array $providerIds, string $configId) { $config = $container->setDefinition($configId, new ChildDefinition('security.firewall.config')); $config->replaceArgument(0, $id); @@ -284,9 +290,9 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a if (isset($firewall['request_matcher'])) { $matcher = new Reference($firewall['request_matcher']); } elseif (isset($firewall['pattern']) || isset($firewall['host'])) { - $pattern = isset($firewall['pattern']) ? $firewall['pattern'] : null; - $host = isset($firewall['host']) ? $firewall['host'] : null; - $methods = isset($firewall['methods']) ? $firewall['methods'] : []; + $pattern = $firewall['pattern'] ?? null; + $host = $firewall['host'] ?? null; + $methods = $firewall['methods'] ?? []; $matcher = $this->createRequestMatcher($container, $pattern, $host, null, $methods); } @@ -321,10 +327,11 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a $listeners[] = new Reference('security.channel_listener'); $contextKey = null; + $contextListenerId = null; // Context serializer listener if (false === $firewall['stateless']) { $contextKey = $firewall['context'] ?? $id; - $listeners[] = new Reference($this->createContextListener($container, $contextKey)); + $listeners[] = new Reference($contextListenerId = $this->createContextListener($container, $contextKey)); $sessionStrategyId = 'security.authentication.session_strategy'; } else { $this->statelessFirewallKeys[] = $id; @@ -394,10 +401,10 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a } // Determine default entry point - $configuredEntryPoint = isset($firewall['entry_point']) ? $firewall['entry_point'] : null; + $configuredEntryPoint = $firewall['entry_point'] ?? null; // Authentication listeners - list($authListeners, $defaultEntryPoint) = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint); + [$authListeners, $defaultEntryPoint] = $this->createAuthenticationListeners($container, $id, $firewall, $authenticationProviders, $defaultProvider, $providerIds, $configuredEntryPoint, $contextListenerId); $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); @@ -406,7 +413,7 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Switch user listener if (isset($firewall['switch_user'])) { $listenerKeys[] = 'switch_user'; - $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless'], $providerIds)); + $listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider, $firewall['stateless'])); } // Access listener @@ -415,8 +422,8 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a // Exception listener $exceptionListener = new Reference($this->createExceptionListener($container, $firewall, $id, $configuredEntryPoint ?: $defaultEntryPoint, $firewall['stateless'])); - $config->replaceArgument(8, isset($firewall['access_denied_handler']) ? $firewall['access_denied_handler'] : null); - $config->replaceArgument(9, isset($firewall['access_denied_url']) ? $firewall['access_denied_url'] : null); + $config->replaceArgument(8, $firewall['access_denied_handler'] ?? null); + $config->replaceArgument(9, $firewall['access_denied_url'] ?? null); $container->setAlias('security.user_checker.'.$id, new Alias($firewall['user_checker'], false)); @@ -429,17 +436,13 @@ private function createFirewall(ContainerBuilder $container, $id, $firewall, &$a } } - if (isset($firewall['anonymous'])) { - $listenerKeys[] = 'anonymous'; - } - $config->replaceArgument(10, $listenerKeys); - $config->replaceArgument(11, isset($firewall['switch_user']) ? $firewall['switch_user'] : null); + $config->replaceArgument(11, $firewall['switch_user'] ?? null); return [$matcher, $listeners, $exceptionListener, null !== $logoutListenerId ? new Reference($logoutListenerId) : null]; } - private function createContextListener($container, $contextKey) + private function createContextListener(ContainerBuilder $container, string $contextKey) { if (isset($this->contextListeners[$contextKey])) { return $this->contextListeners[$contextKey]; @@ -452,7 +455,7 @@ private function createContextListener($container, $contextKey) return $this->contextListeners[$contextKey] = $listenerId; } - private function createAuthenticationListeners($container, $id, $firewall, &$authenticationProviders, $defaultProvider = null, array $providerIds, $defaultEntryPoint) + private function createAuthenticationListeners(ContainerBuilder $container, string $id, array $firewall, array &$authenticationProviders, ?string $defaultProvider, array $providerIds, ?string $defaultEntryPoint, string $contextListenerId = null) { $listeners = []; $hasListeners = false; @@ -467,9 +470,13 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut throw new InvalidConfigurationException(sprintf('Invalid firewall "%s": user provider "%s" not found.', $id, $firewall[$key]['provider'])); } $userProvider = $providerIds[$normalizedName]; - } elseif ('remember_me' === $key) { - // RememberMeFactory will use the firewall secret when created + } elseif ('remember_me' === $key || 'anonymous' === $key) { + // RememberMeFactory will use the firewall secret when created, AnonymousAuthenticationListener does not load users. $userProvider = null; + + if ('remember_me' === $key && $contextListenerId) { + $container->getDefinition($contextListenerId)->addTag('security.remember_me_aware', ['id' => $id, 'provider' => 'none']); + } } elseif ($defaultProvider) { $userProvider = $defaultProvider; } elseif (empty($providerIds)) { @@ -479,7 +486,7 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut throw new InvalidConfigurationException(sprintf('Not configuring explicitly the provider for the "%s" listener on "%s" firewall is ambiguous as there is more than one registered provider.', $key, $id)); } - list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); + [$provider, $listenerId, $defaultEntryPoint] = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint); $listeners[] = new Reference($listenerId); $authenticationProviders[] = $provider; @@ -488,30 +495,6 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut } } - // Anonymous - if (isset($firewall['anonymous'])) { - if (null === $firewall['anonymous']['secret']) { - $firewall['anonymous']['secret'] = new Parameter('container.build_hash'); - } - - $listenerId = 'security.authentication.listener.anonymous.'.$id; - $container - ->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.anonymous')) - ->replaceArgument(1, $firewall['anonymous']['secret']) - ; - - $listeners[] = new Reference($listenerId); - - $providerId = 'security.authentication.provider.anonymous.'.$id; - $container - ->setDefinition($providerId, new ChildDefinition('security.authentication.provider.anonymous')) - ->replaceArgument(0, $firewall['anonymous']['secret']) - ; - - $authenticationProviders[] = $providerId; - $hasListeners = true; - } - if (false === $hasListeners) { throw new InvalidConfigurationException(sprintf('No authentication listener registered for firewall "%s".', $id)); } @@ -519,11 +502,11 @@ private function createAuthenticationListeners($container, $id, $firewall, &$aut return [$listeners, $defaultEntryPoint]; } - private function createEncoders($encoders, ContainerBuilder $container) + private function createEncoders(array $encoders, ContainerBuilder $container) { $encoderMap = []; foreach ($encoders as $class => $encoder) { - $encoderMap[$class] = $this->createEncoder($encoder, $container); + $encoderMap[$class] = $this->createEncoder($encoder); } $container @@ -532,13 +515,17 @@ private function createEncoders($encoders, ContainerBuilder $container) ; } - private function createEncoder($config, ContainerBuilder $container) + private function createEncoder(array $config) { // a custom encoder service if (isset($config['id'])) { return new Reference($config['id']); } + if ($config['migrate_from'] ?? false) { + return $config; + } + // plaintext encoder if ('plaintext' === $config['algorithm']) { $arguments = [$config['ignore_case']]; @@ -564,34 +551,37 @@ private function createEncoder($config, ContainerBuilder $container) // bcrypt encoder if ('bcrypt' === $config['algorithm']) { - @trigger_error('Configuring an encoder with "bcrypt" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED); + $config['algorithm'] = 'native'; + $config['native_algorithm'] = \PASSWORD_BCRYPT; - return [ - 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', - 'arguments' => [$config['cost'] ?? 13], - ]; + return $this->createEncoder($config); } // Argon2i encoder if ('argon2i' === $config['algorithm']) { - @trigger_error('Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead.', E_USER_DEPRECATED); + if (SodiumPasswordEncoder::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 InvalidConfigurationException(sprintf('Algorithm "argon2i" is not available. Either use "%s" or upgrade to PHP 7.2+ instead.', \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13') ? 'argon2id", "auto' : 'auto')); + } - if (!Argon2iPasswordEncoder::isSupported()) { - if (\extension_loaded('sodium') && !\defined('SODIUM_CRYPTO_PWHASH_SALTBYTES')) { - throw new InvalidConfigurationException('The installed libsodium version does not have support for Argon2i. Use "auto" instead.'); - } + return $this->createEncoder($config); + } - throw new InvalidConfigurationException('Argon2i algorithm is not supported. Install the libsodium extension or use "auto" instead.'); + if ('argon2id' === $config['algorithm']) { + if (($hasSodium = SodiumPasswordEncoder::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 InvalidConfigurationException(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 [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', - 'arguments' => [ - $config['memory_cost'], - $config['time_cost'], - $config['threads'], - ], - ]; + return $this->createEncoder($config); } if ('native' === $config['algorithm']) { @@ -601,7 +591,7 @@ private function createEncoder($config, ContainerBuilder $container) $config['time_cost'], (($config['memory_cost'] ?? 0) << 10) ?: null, $config['cost'], - ], + ] + (isset($config['native_algorithm']) ? [3 => $config['native_algorithm']] : []), ]; } @@ -624,7 +614,7 @@ private function createEncoder($config, ContainerBuilder $container) } // Parses user providers and returns an array of their ids - private function createUserProviders($config, ContainerBuilder $container) + private function createUserProviders(array $config, ContainerBuilder $container): array { $providerIds = []; foreach ($config['providers'] as $name => $provider) { @@ -636,7 +626,7 @@ private function createUserProviders($config, ContainerBuilder $container) } // Parses a <provider> tag and returns the id for the related user provider service - private function createUserDaoProvider($name, $provider, ContainerBuilder $container) + private function createUserDaoProvider(string $name, array $provider, ContainerBuilder $container): string { $name = $this->getUserProviderId($name); @@ -672,15 +662,15 @@ private function createUserDaoProvider($name, $provider, ContainerBuilder $conta return $name; } - throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider', $name)); + throw new InvalidConfigurationException(sprintf('Unable to create definition for "%s" user provider.', $name)); } - private function getUserProviderId($name) + private function getUserProviderId(string $name): string { return 'security.user.provider.concrete.'.strtolower($name); } - private function createExceptionListener($container, $config, $id, $defaultEntryPoint, $stateless) + private function createExceptionListener(ContainerBuilder $container, array $config, string $id, ?string $defaultEntryPoint, bool $stateless): string { $exceptionListenerId = 'security.exception_listener.'.$id; $listener = $container->setDefinition($exceptionListenerId, new ChildDefinition('security.exception_listener')); @@ -698,7 +688,7 @@ private function createExceptionListener($container, $config, $id, $defaultEntry return $exceptionListenerId; } - private function createSwitchUserListener($container, $id, $config, $defaultProvider, $stateless, $providerIds) + private function createSwitchUserListener(ContainerBuilder $container, string $id, array $config, ?string $defaultProvider, bool $stateless): string { $userProvider = isset($config['provider']) ? $this->getUserProviderId($config['provider']) : $defaultProvider; @@ -718,13 +708,13 @@ private function createSwitchUserListener($container, $id, $config, $defaultProv return $switchUserListenerId; } - private function createExpression($container, $expression) + private function createExpression(ContainerBuilder $container, string $expression): Reference { if (isset($this->expressions[$id = '.security.expression.'.ContainerBuilder::hash($expression)])) { return $this->expressions[$id]; } - if (!class_exists('Symfony\Component\ExpressionLanguage\ExpressionLanguage')) { + if (!class_exists(\Symfony\Component\ExpressionLanguage\ExpressionLanguage::class)) { throw new \RuntimeException('Unable to use expressions as the Symfony ExpressionLanguage component is not installed.'); } @@ -737,10 +727,10 @@ private function createExpression($container, $expression) return $this->expressions[$id] = new Reference($id); } - private function createRequestMatcher(ContainerBuilder $container, $path = null, $host = null, int $port = null, $methods = [], array $ips = null, array $attributes = []) + private function createRequestMatcher(ContainerBuilder $container, string $path = null, string $host = null, int $port = null, array $methods = [], array $ips = null, array $attributes = []): Reference { if ($methods) { - $methods = array_map('strtoupper', (array) $methods); + $methods = array_map('strtoupper', $methods); } if (null !== $ips) { @@ -787,9 +777,7 @@ public function addUserProviderFactory(UserProviderFactoryInterface $factory) } /** - * Returns the base path for the XSD files. - * - * @return string The XSD base path + * {@inheritdoc} */ public function getXsdValidationBasePath() { @@ -812,7 +800,7 @@ private function isValidIp(string $cidr): bool $cidrParts = explode('/', $cidr); if (1 === \count($cidrParts)) { - return false !== filter_var($cidrParts[0], FILTER_VALIDATE_IP); + return false !== filter_var($cidrParts[0], \FILTER_VALIDATE_IP); } $ip = $cidrParts[0]; @@ -822,11 +810,11 @@ private function isValidIp(string $cidr): bool return false; } - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4)) { + if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { return $netmask <= 32; } - if (filter_var($ip, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) { + if (filter_var($ip, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { return $netmask <= 128; } diff --git a/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php b/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php index 30956dafcfb9a..1b37d92373705 100644 --- a/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php +++ b/src/Symfony/Bundle/SecurityBundle/EventListener/VoteListener.php @@ -33,15 +33,13 @@ public function __construct(TraceableAccessDecisionManager $traceableAccessDecis /** * Event dispatched by a voter during access manager decision. - * - * @param VoteEvent $event event with voter data */ public function onVoterVote(VoteEvent $event) { $this->traceableAccessDecisionManager->addVoterVote($event->getVoter(), $event->getAttributes(), $event->getVote()); } - public static function getSubscribedEvents() + public static function getSubscribedEvents(): array { return ['debug.security.authorization.vote' => 'onVoterVote']; } diff --git a/src/Symfony/Bundle/SecurityBundle/LICENSE b/src/Symfony/Bundle/SecurityBundle/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bundle/SecurityBundle/LICENSE +++ b/src/Symfony/Bundle/SecurityBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/SecurityBundle/README.md b/src/Symfony/Bundle/SecurityBundle/README.md index 6f36866ec8b78..63b502f87ba5c 100644 --- a/src/Symfony/Bundle/SecurityBundle/README.md +++ b/src/Symfony/Bundle/SecurityBundle/README.md @@ -1,10 +1,13 @@ SecurityBundle ============== +SecurityBundle provides a tight integration of the Security component into the +Symfony full-stack framework. + Resources --------- - * [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) + * [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/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml index 2effc4554bb26..811c6dfc5cfdc 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/collectors.xml @@ -9,7 +9,7 @@ <service id="data_collector.security" class="Symfony\Bundle\SecurityBundle\DataCollector\SecurityDataCollector"> <tag name="data_collector" template="@Security/Collector/security.html.twig" id="security" priority="270" /> - <argument type="service" id="security.token_storage" on-invalid="ignore" /> + <argument type="service" id="security.untracked_token_storage" /> <argument type="service" id="security.role_hierarchy" /> <argument type="service" id="security.logout_url_generator" /> <argument type="service" id="security.access.decision_manager" /> diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml index 43321494e0194..2fae1438991cd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/guard.xml @@ -17,7 +17,7 @@ <argument type="service" id="security.authentication.session_strategy" /> </call> </service> - + <service id="Symfony\Component\Security\Guard\GuardAuthenticatorHandler" alias="security.authentication.guard_handler" /> <!-- See GuardAuthenticationFactory --> @@ -29,6 +29,7 @@ <argument /> <!-- User Provider --> <argument /> <!-- Provider-shared Key --> <argument /> <!-- User Checker --> + <argument type="service" id="security.password_encoder" /> </service> <service id="security.authentication.listener.guard" @@ -41,6 +42,7 @@ <argument /> <!-- Provider-shared Key --> <argument /> <!-- Authenticator --> <argument type="service" id="logger" on-invalid="null" /> + <argument>%security.authentication.hide_user_not_found%</argument> </service> </services> </container> diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index 021acccb2a14b..eabe5e547fada 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -21,11 +21,18 @@ </service> <service id="Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface" alias="security.authorization_checker" /> - <service id="security.token_storage" class="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage" public="true"> + <service id="security.token_storage" class="Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage" public="true"> + <tag name="kernel.reset" method="disableUsageTracking" /> <tag name="kernel.reset" method="setToken" /> + <argument type="service" id="security.untracked_token_storage" /> + <argument type="service_locator"> + <argument key="session" type="service" id="session" /> + </argument> </service> <service id="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface" alias="security.token_storage" /> + <service id="security.untracked_token_storage" class="Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage" /> + <service id="security.helper" class="Symfony\Component\Security\Core\Security"> <argument type="service_locator"> <argument key="security.token_storage" type="service" id="security.token_storage" /> @@ -56,6 +63,7 @@ <service id="security.authentication.session_strategy" class="Symfony\Component\Security\Http\Session\SessionAuthenticationStrategy"> <argument>%security.authentication.session_strategy.strategy%</argument> + <argument type="service" id="security.csrf.token_storage" on-invalid="ignore" /> </service> <service id="Symfony\Component\Security\Http\Session\SessionAuthenticationStrategyInterface" alias="security.authentication.session_strategy" /> @@ -80,7 +88,7 @@ <service id="security.user_checker" class="Symfony\Component\Security\Core\User\UserChecker" /> <service id="security.expression_language" class="Symfony\Component\Security\Core\Authorization\ExpressionLanguage"> - <argument type="service" id="cache.security_expression_language"></argument> + <argument type="service" id="cache.security_expression_language" on-invalid="null" /> </service> <service id="security.authentication_utils" class="Symfony\Component\Security\Http\Authentication\AuthenticationUtils" public="true"> @@ -144,25 +152,33 @@ <argument /> <!-- FirewallConfig --> </service> + <service id="security.firewall.lazy_context" class="Symfony\Bundle\SecurityBundle\Security\LazyFirewallContext" abstract="true"> + <argument type="collection" /> + <argument type="service" id="security.exception_listener" /> + <argument /> <!-- LogoutListener --> + <argument /> <!-- FirewallConfig --> + <argument type="service" id="security.untracked_token_storage" /> + </service> + <service id="security.firewall.config" class="Symfony\Bundle\SecurityBundle\Security\FirewallConfig" abstract="true"> <argument /> <!-- name --> <argument /> <!-- user_checker --> <argument /> <!-- request_matcher --> - <argument /> <!-- security enabled --> - <argument /> <!-- stateless --> + <argument>false</argument> <!-- security enabled --> + <argument>false</argument> <!-- stateless --> <argument /> <!-- provider --> <argument /> <!-- context --> <argument /> <!-- entry_point --> <argument /> <!-- access_denied_handler --> <argument /> <!-- access_denied_url --> <argument type="collection" /> <!-- listeners --> - <argument /> <!-- switch_user --> + <argument>null</argument> <!-- switch_user --> </service> <service id="security.logout_url_generator" class="Symfony\Component\Security\Http\Logout\LogoutUrlGenerator"> <argument type="service" id="request_stack" on-invalid="null" /> <argument type="service" id="router" on-invalid="null" /> - <argument type="service" id="security.token_storage" on-invalid="null" /> + <argument type="service" id="security.token_storage" /> </service> <!-- Provisioning --> @@ -175,7 +191,7 @@ <deprecated>The "%service_id%" service is deprecated since Symfony 4.1.</deprecated> </service> - <service id="security.user.provider.ldap" class="Symfony\Component\Security\Core\User\LdapUserProvider" abstract="true"> + <service id="security.user.provider.ldap" class="Symfony\Component\Ldap\Security\LdapUserProvider" abstract="true"> <argument /> <!-- security.ldap.ldap --> <argument /> <!-- base dn --> <argument /> <!-- search dn --> diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 55044986e3107..3cd2bfabdfbf6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -9,7 +9,7 @@ <service id="security.authentication.listener.anonymous" class="Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener"> <tag name="monolog.logger" channel="security" /> - <argument type="service" id="security.token_storage" /> + <argument type="service" id="security.untracked_token_storage" /> <argument /> <!-- Key --> <argument type="service" id="logger" on-invalid="null" /> <argument type="service" id="security.authentication.manager" /> @@ -37,7 +37,7 @@ <service id="security.context_listener" class="Symfony\Component\Security\Http\Firewall\ContextListener"> <tag name="monolog.logger" channel="security" /> - <argument type="service" id="security.token_storage" /> + <argument type="service" id="security.untracked_token_storage" /> <argument type="collection" /> <argument /> <!-- Provider Key --> <argument type="service" id="logger" on-invalid="null" /> @@ -88,6 +88,7 @@ <service id="security.authentication.success_handler" class="Symfony\Component\Security\Http\Authentication\DefaultAuthenticationSuccessHandler" abstract="true"> <argument type="service" id="security.http_utils" /> <argument type="collection" /> <!-- Options --> + <argument type="service" id="logger" on-invalid="null" /> </service> <service id="security.authentication.custom_failure_handler" class="Symfony\Component\Security\Http\Authentication\CustomAuthenticationFailureHandler" abstract="true"> @@ -128,7 +129,7 @@ <service id="security.authentication.listener.simple_preauth" class="Symfony\Component\Security\Http\Firewall\SimplePreAuthenticationListener" abstract="true"> <tag name="monolog.logger" channel="security" /> - <argument type="service" id="security.token_storage" /> + <argument type="service" id="security.untracked_token_storage" /> <argument type="service" id="security.authentication.manager" /> <argument /> <!-- Provider-shared Key --> <argument /> <!-- Authenticator --> @@ -195,10 +196,10 @@ <argument /> <!-- UserChecker --> <argument /> <!-- Provider-shared Key --> <argument /> <!-- LDAP --> - <argument /> <!-- search dn --> - <argument /> <!-- search password --> <argument /> <!-- Base DN --> <argument>%security.authentication.hide_user_not_found%</argument> + <argument /> <!-- search dn --> + <argument /> <!-- search password --> </service> <service id="security.authentication.provider.simple" class="Symfony\Component\Security\Core\Authentication\Provider\SimpleAuthenticationProvider" abstract="true"> diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml index 956a75a5be2ba..94aa3a3824ba4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_rememberme.xml @@ -9,7 +9,7 @@ <service id="security.authentication.listener.rememberme" class="Symfony\Component\Security\Http\Firewall\RememberMeListener" abstract="true"> <tag name="monolog.logger" channel="security" /> - <argument type="service" id="security.token_storage" /> + <argument type="service" id="security.untracked_token_storage" /> <argument type="service" id="security.authentication.rememberme" /> <argument type="service" id="security.authentication.manager" /> <argument type="service" id="logger" on-invalid="null" /> diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig index 6b0819513fa04..59ebea428b7f6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -5,9 +5,9 @@ {% block toolbar %} {% if collector.token %} {% set is_authenticated = collector.enabled and collector.authenticated %} - {% set color_code = is_authenticated ? '' : 'yellow' %} + {% set color_code = not is_authenticated ? 'yellow' %} {% else %} - {% set color_code = collector.enabled ? 'red' : '' %} + {% set color_code = collector.enabled ? 'red' %} {% endif %} {% set icon %} diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php index d811faac4bf06..25cc078af2da2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallContext.php @@ -36,12 +36,12 @@ public function __construct(iterable $listeners, ExceptionListener $exceptionLis $this->exceptionListener = $exceptionListener; if ($logoutListener instanceof FirewallConfig) { $this->config = $logoutListener; - @trigger_error(sprintf('Passing an instance of %s as the 3rd argument to "%s()" is deprecated since Symfony 4.2. Pass a %s instance instead.', FirewallConfig::class, __METHOD__, LogoutListener::class), E_USER_DEPRECATED); + @trigger_error(sprintf('Passing an instance of %s as the 3rd argument to "%s()" is deprecated since Symfony 4.2. Pass a %s instance instead.', FirewallConfig::class, __METHOD__, LogoutListener::class), \E_USER_DEPRECATED); } elseif (null === $logoutListener || $logoutListener instanceof LogoutListener) { $this->logoutListener = $logoutListener; $this->config = $config; } else { - throw new \TypeError(sprintf('Argument 3 passed to %s() must be instance of %s or null, %s given.', __METHOD__, LogoutListener::class, \is_object($logoutListener) ? \get_class($logoutListener) : \gettype($logoutListener))); + throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be instance of "%s" or null, "%s" given.', __METHOD__, LogoutListener::class, \is_object($logoutListener) ? \get_class($logoutListener) : \gettype($logoutListener))); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php index d33cfdc7b1fda..06cbc64d18c6b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php +++ b/src/Symfony/Bundle/SecurityBundle/Security/FirewallMap.php @@ -26,13 +26,11 @@ class FirewallMap implements FirewallMapInterface { private $container; private $map; - private $contexts; public function __construct(ContainerInterface $container, iterable $map) { $this->container = $container; $this->map = $map; - $this->contexts = new \SplObjectStorage(); } public function getListeners(Request $request) @@ -54,16 +52,13 @@ public function getFirewallConfig(Request $request) $context = $this->getFirewallContext($request); if (null === $context) { - return; + return null; } return $context->getConfig(); } - /** - * @return FirewallContext - */ - private function getFirewallContext(Request $request) + private function getFirewallContext(Request $request): ?FirewallContext { if ($request->attributes->has('_firewall_context')) { $storedContextId = $request->attributes->get('_firewall_context'); @@ -83,5 +78,7 @@ private function getFirewallContext(Request $request) return $this->container->get($contextId); } } + + return null; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php new file mode 100644 index 0000000000000..65144b77494ac --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Security/LazyFirewallContext.php @@ -0,0 +1,81 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Security; + +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Http\Event\LazyResponseEvent; +use Symfony\Component\Security\Http\Firewall\Abs 10000 tractListener; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Symfony\Component\Security\Http\Firewall\LogoutListener; + +/** + * Lazily calls authentication listeners when actually required by the access listener. + * + * @author Nicolas Grekas <p@tchwork.com> + */ +class LazyFirewallContext extends FirewallContext +{ + private $tokenStorage; + + public function __construct(iterable $listeners, ?ExceptionListener $exceptionListener, ?LogoutListener $logoutListener, ?FirewallConfig $config, TokenStorage $tokenStorage) + { + parent::__construct($listeners, $exceptionListener, $logoutListener, $config); + + $this->tokenStorage = $tokenStorage; + } + + public function getListeners(): iterable + { + return [$this]; + } + + public function __invoke(RequestEvent $event) + { + $listeners = []; + $request = $event->getRequest(); + $lazy = $request->isMethodCacheable(); + + foreach (parent::getListeners() as $listener) { + if (!\is_callable($listener)) { + @trigger_error(sprintf('Calling the "%s::handle()" method from the firewall is deprecated since Symfony 4.3, extend "%s" instead.', \get_class($listener), AbstractListener::class), \E_USER_DEPRECATED); + $listeners[] = [$listener, 'handle']; + $lazy = false; + } elseif (!$lazy || !$listener instanceof AbstractListener) { + $listeners[] = $listener; + $lazy = $lazy && $listener instanceof AbstractListener; + } elseif (false !== $supports = $listener->supports($request)) { + $listeners[] = [$listener, 'authenticate']; + $lazy = null === $supports; + } + } + + if (!$lazy) { + foreach ($listeners as $listener) { + $listener($event); + + if ($event->hasResponse()) { + return; + } + } + + return; + } + + $this->tokenStorage->setInitializer(function () use ($event, $listeners) { + $event = new LazyResponseEvent($event); + foreach ($listeners as $listener) { + $listener($event); + } + }); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index e5035d765d5e3..24cf5569b819b 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,6 +15,8 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; @@ -31,7 +33,14 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory; use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\EventDispatcher\DependencyInjection\AddEventAliasesPass; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\SwitchUserEvent; +use Symfony\Component\Security\Http\SecurityEvents; /** * Bundle. @@ -57,6 +66,7 @@ public function build(ContainerBuilder $container) $extension->addSecurityListenerFactory(new SimplePreAuthenticationFactory(false)); $extension->addSecurityListenerFactory(new SimpleFormFactory(false)); $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); + $extension->addSecurityListenerFactory(new AnonymousFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); $extension->addUserProviderFactory(new LdapFactory()); @@ -64,5 +74,13 @@ public function build(ContainerBuilder $container) $container->addCompilerPass(new AddSecurityVotersPass()); $container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass()); + $container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200); + + $container->addCompilerPass(new AddEventAliasesPass([ + AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS, + AuthenticationFailureEvent::class => AuthenticationEvents::AUTHENTICATION_FAILURE, + InteractiveLoginEvent::class => SecurityEvents::INTERACTIVE_LOGIN, + SwitchUserEvent::class => SecurityEvents::SWITCH_USER, + ])); } } diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php b/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php index 476e24ee4e456..939b2f2dfcbe6 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityUserValueResolver.php @@ -19,7 +19,7 @@ use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Http\Controller\UserValueResolver; -@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1, use "%s" instead.', SecurityUserValueResolver::class, UserValueResolver::class), E_USER_DEPRECATED); +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.1, use "%s" instead.', SecurityUserValueResolver::class, UserValueResolver::class), \E_USER_DEPRECATED); /** * Supports the argument type of {@see UserInterface}. @@ -37,7 +37,7 @@ public function __construct(TokenStorageInterface $tokenStorage) $this->tokenStorage = $tokenStorage; } - public function supports(Request $request, ArgumentMetadata $argument) + public function supports(Request $request, ArgumentMetadata $argument): bool { // only security user implementations are supported if (UserInterface::class !== $argument->getType()) { @@ -55,7 +55,7 @@ public function supports(Request $request, ArgumentMetadata $argument) return $user instanceof UserInterface; } - public function resolve(Request $request, ArgumentMetadata $argument) + public function resolve(Request $request, ArgumentMetadata $argument): iterable { yield $this->tokenStorage->getToken()->getUser(); } diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php index 30ef9c2829028..0e8e518fee27a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/LogoutUrlHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Templating\Helper; -@trigger_error('The '.LogoutUrlHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.LogoutUrlHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Security\Http\Logout\LogoutUrlGenerator; use Symfony\Component\Templating\Helper\Helper; diff --git a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php index f6738bd3690df..5a996d64e63a0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php +++ b/src/Symfony/Bundle/SecurityBundle/Templating/Helper/SecurityHelper.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Templating\Helper; -@trigger_error('The '.SecurityHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.SecurityHelper::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Symfony\Component\Security\Acl\Voter\FieldVote; use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php index d1299bffed611..9143db38edffd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/CacheWarmer/ExpressionCacheWarmerTest.php @@ -26,8 +26,8 @@ public function testWarmUp() $expressionLang->expects($this->exactly(2)) ->method('parse') ->withConsecutive( - [$expressions[0], ['token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver']], - [$expressions[1], ['token', 'user', 'object', 'subject', 'roles', 'request', 'trust_resolver']] + [$expressions[0], ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']], + [$expressions[1], ['token', 'user', 'object', 'subject', 'roles', 'role_names', 'request', 'trust_resolver']] ); (new ExpressionCacheWarmer($expressions, $expressionLang))->warmUp(''); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php index 7e583facf7b09..74bb0404a8e8a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DataCollector/SecurityDataCollectorTest.php @@ -220,7 +220,7 @@ public function testGetFirewallReturnsNull() public function testGetListeners() { $request = new Request(); - $event = new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); $event->setResponse($response = new Response()); $listener = function ($e) use ($event, &$listenerCalled) { $listenerCalled += $e === $event; @@ -258,7 +258,6 @@ public function providerCollectDecisionLog(): \Generator $eventDispatcher = $this->getMockBuilder(EventDispatcherInterface::class)->getMockForAbstractClass(); $decoratedVoter1 = new TraceableVoter($voter1, $eventDispatcher); - $decoratedVoter2 = new TraceableVoter($voter2, $eventDispatcher); yield [ AccessDecisionManager::STRATEGY_AFFIRMATIVE, @@ -346,7 +345,7 @@ public function providerCollectDecisionLog(): \Generator * * @dataProvider providerCollectDecisionLog */ - public function testCollectDecisionLog(string $strategy, array $decisionLog, array $voters, array $expectedVoterClasses, array $expectedDecisionLog): void + public function testCollectDecisionLog(string $strategy, array $decisionLog, array $voters, array $expectedVoterClasses, array $expectedDecisionLog) { $accessDecisionManager = $this ->getMockBuilder(TraceableAccessDecisionManager::class) diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php index b7c6bcc076631..c1be247e812f7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Debug/TraceableFirewallListenerTest.php @@ -29,15 +29,12 @@ class TraceableFirewallListenerTest extends TestCase public function testOnKernelRequestRecordsListeners() { $request = new Request(); - $event = new RequestEvent($this->getMockBuilder(HttpKernelInterface::class)->getMock(), $request, HttpKernelInterface::MASTER_REQUEST); + $event = new RequestEvent($this->createMock(HttpKernelInterface::class), $request, HttpKernelInterface::MASTER_REQUEST); $event->setResponse($response = new Response()); $listener = function ($e) use ($event, &$listenerCalled) { $listenerCalled += $e === $event; }; - $firewallMap = $this - ->getMockBuilder(FirewallMap::class) - ->disableOriginalConstructor() - ->getMock(); + $firewallMap = $this->createMock(FirewallMap::class); $firewallMap ->expects($this->once()) ->method('getFirewallConfig') diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php index 93d412155385f..62e1c9cfcf721 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/AddSecurityVotersPassTest.php @@ -15,18 +15,17 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\Voter\Voter; class AddSecurityVotersPassTest extends TestCase { - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage No security voters found. You need to tag at least one with "security.voter". - */ public function testNoVoters() { + $this->expectException(LogicException::class); + $this->expectExceptionMessage('No security voters found. You need to tag at least one with "security.voter".'); $container = new ContainerBuilder(); $container ->register('security.access.decision_manager', AccessDecisionManager::class) @@ -72,7 +71,7 @@ public function testThatSecurityVotersAreProcessedInPriorityOrder() $this->assertCount(4, $refs); } - public function testThatVotersAreTraceableInDebugMode(): void + public function testThatVotersAreTraceableInDebugMode() { $container = new ContainerBuilder(); @@ -104,7 +103,7 @@ public function testThatVotersAreTraceableInDebugMode(): void $this->assertCount(2, $voters, 'Incorrect count of voters'); } - public function testThatVotersAreNotTraceableWithoutDebugMode(): void + public function testThatVotersAreNotTraceableWithoutDebugMode() { $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); @@ -128,12 +127,14 @@ public function testThatVotersAreNotTraceableWithoutDebugMode(): void $this->assertFalse($container->has('debug.security.voter.voter2'), 'voter2 should not be traced'); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - * @expectedExceptionMessage stdClass must implement the Symfony\Component\Security\Core\Authorization\Voter\VoterInterface when used as a voter. - */ public function testVoterMissingInterface() { + $exception = LogicException::class; + $message = '"stdClass" must implement the "Symfony\Component\Security\Core\Authorization\Voter\VoterInterface" when used as a voter.'; + + $this->expectException($exception); + $this->expectExceptionMessage($message); + $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); $container @@ -148,10 +149,3 @@ public function testVoterMissingInterface() $compilerPass->process($container); } } - -class VoterWithoutInterface -{ - public function vote() - { - } -} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php new file mode 100644 index 0000000000000..afdbf9afaf60f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Compiler/RegisterTokenUsageTrackingPassTest.php @@ -0,0 +1,89 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass; +use Symfony\Component\DependencyInjection\Alias; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\HttpFoundation\Session\Session; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Symfony\Component\Security\Core\Authentication\Token\Storage\UsageTrackingTokenStorage; +use Symfony\Component\Security\Http\Firewall\ContextListener; + +class RegisterTokenUsageTrackingPassTest extends TestCase +{ + public function testTokenStorageIsUntrackedIfSessionIsMissing() + { + $container = new ContainerBuilder(); + $container->register('security.untracked_token_storage', TokenStorage::class); + + $compilerPass = new RegisterTokenUsageTrackingPass(); + $compilerPass->process($container); + + $this->assertTrue($container->hasAlias('security.token_storage')); + $this->assertEquals(new Alias('security.untracked_token_storage', true), $container->getAlias('security.token_storage')); + } + + public function testContextListenerIsNotModifiedIfTokenStorageDoesNotSupportUsageTracking() + { + $container = new ContainerBuilder(); + + $container->setParameter('security.token_storage.class', TokenStorage::class); + $container->register('session', Session::class); + $container->register('security.context_listener', ContextListener::class) + ->setArguments([ + new Reference('security.untracked_token_storage'), + [], + 'main', + new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('event_dispatcher', ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('security.authentication.trust_resolver'), + ]); + $container->register('security.token_storage', '%security.token_storage.class%'); + $container->register('security.untracked_token_storage', TokenStorage::class); + + $compilerPass = new RegisterTokenUsageTrackingPass(); + $compilerPass->process($container); + + $this->assertCount(6, $container->getDefinition('security.context_listener')->getArguments()); + } + + public function testContextListenerEnablesUsageTrackingIfSupportedByTokenStorage() + { + $container = new ContainerBuilder(); + + $container->setParameter('security.token_storage.class', UsageTrackingTokenStorage::class); + $container->register('session', Session::class); + $container->register('security.context_listener', ContextListener::class) + ->setArguments([ + new Reference('security.untracked_token_storage'), + [], + 'main', + new Reference('logger', ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('event_dispatcher', ContainerInterface::NULL_ON_INVALID_REFERENCE), + new Reference('security.authentication.trust_resolver'), + ]); + $container->register('security.token_storage', '%security.token_storage.class%'); + $container->register('security.untracked_token_storage', TokenStorage::class); + + $compilerPass = new RegisterTokenUsageTrackingPass(); + $compilerPass->process($container); + + $contextListener = $container->getDefinition('security.context_listener'); + + $this->assertCount(7, $container->getDefinition('security.context_listener')->getArguments()); + $this->assertEquals([new Reference('security.token_storage'), 'enableUsageTracking'], $contextListener->getArgument(6)); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php index ef318946ce66c..b3cfbb9f88d71 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/CompleteConfigurationTest.php @@ -14,11 +14,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; -use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; +use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\SodiumPasswordEncoder; abstract class CompleteConfigurationTest extends TestCase @@ -41,7 +42,7 @@ public function testUserProviders() { $container = $this->getContainer('container1'); - $providers = array_values(array_filter($container->getServiceIds(), function ($key) { return 0 === strpos($key, 'security.user.provider.concrete'); })); + $providers = array_values(array_filter($container->getServiceIds(), function ($key) { return str_starts_with($key, 'security.user.provider.concrete'); })); $expectedProviders = [ 'security.user.provider.concrete.default', @@ -56,7 +57,7 @@ public function testUserProviders() // chain provider $this->assertEquals([new IteratorArgument([ - new Reference('security.user.provider.concrete.service'), + new Reference('user.manager'), new Reference('security.user.provider.concrete.basic'), ])], $container->getDefinition('security.user.provider.concrete.chain')->getArguments()); } @@ -70,9 +71,9 @@ public function testFirewalls() foreach (array_keys($arguments[1]->getValues()) as $contextId) { $contextDef = $container->getDefinition($contextId); $arguments = $contextDef->getArguments(); - $listeners[] = array_map('strval', $arguments['index_0']->getValues()); + $listeners[] = array_map('strval', $arguments[0]->getValues()); - $configDef = $container->getDefinition((string) $arguments['index_3']); + $configDef = $container->getDefinition((string) $arguments[3]); $configs[] = array_values($configDef->getArguments()); } @@ -87,6 +88,14 @@ public function testFirewalls() 'security.user_checker', '.security.request_matcher.xmi9dcw', false, + false, + '', + '', + '', + '', + '', + [], + null, ], [ 'secure', @@ -228,7 +237,7 @@ public function testAccess() } $matcherIds = []; - foreach ($rules as list($matcherId, $attributes, $channel)) { + foreach ($rules as [$matcherId, $attributes, $channel]) { $requestMatcher = $container->getDefinition($matcherId); $this->assertArrayNotHasKey($matcherId, $matcherIds); @@ -287,6 +296,7 @@ public function testEncoders() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ 'algorithm' => 'md5', @@ -299,6 +309,7 @@ public function testEncoders() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), 'JMS\FooBundle\Entity\User5' => [ @@ -320,6 +331,7 @@ public function testEncoders() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } @@ -348,6 +360,7 @@ public function testEncodersWithLibsodium() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ 'algorithm' => 'md5', @@ -360,6 +373,7 @@ public function testEncodersWithLibsodium() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), 'JMS\FooBundle\Entity\User5' => [ @@ -377,14 +391,9 @@ public function testEncodersWithLibsodium() ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } - /** - * @group legacy - * - * @expectedDeprecation Configuring an encoder with "argon2i" as algorithm is deprecated since Symfony 4.3, use "auto" instead. - */ public function testEncodersWithArgon2i() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm is not supported.'); } @@ -406,6 +415,7 @@ public function testEncodersWithArgon2i() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ 'algorithm' => 'md5', @@ -418,6 +428,7 @@ public function testEncodersWithArgon2i() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), 'JMS\FooBundle\Entity\User5' => [ @@ -429,15 +440,76 @@ public function testEncodersWithArgon2i() 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder', - 'arguments' => [256, 1, 2], + 'class' => $sodium ? SodiumPasswordEncoder::class : NativePasswordEncoder::class, + 'arguments' => $sodium ? [256, 1] : [1, 262144, null, \PASSWORD_ARGON2I], + ], + ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); + } + + public function testMigratingEncoder() + { + if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { + $this->markTestSkipped('Argon2i algorithm is not supported.'); + } + + $container = $this->getContainer('migrating_encoder'); + + $this->assertEquals([[ + 'JMS\FooBundle\Entity\User1' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', + 'arguments' => [false], + ], + 'JMS\FooBundle\Entity\User2' => [ + 'algorithm' => 'sha1', + 'encode_as_base64' => false, + 'iterations' => 5, + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'cost' => null, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + 'migrate_from' => [], + ], + 'JMS\FooBundle\Entity\User3' => [ + 'algorithm' => 'md5', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => null, + 'memory_cost' => null, + 'time_cost' => null, + 'threads' => null, + 'migrate_from' => [], + ], + 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), + 'JMS\FooBundle\Entity\User5' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder', + 'arguments' => ['sha1', false, 5, 30], + ], + 'JMS\FooBundle\Entity\User6' => [ + 'class' => 'Symfony\Component\Security\Core\Encoder\NativePasswordEncoder', + 'arguments' => [8, 102400, 15], + ], + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'argon2i', + 'hash_algorithm' => 'sha512', + 'key_length' => 40, + 'ignore_case' => false, + 'encode_as_base64' => true, + 'iterations' => 5000, + 'cost' => null, + 'memory_cost' => 256, + 'time_cost' => 1, + 'threads' => null, + 'migrate_from' => ['bcrypt'], ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } - /** - * @group legacy - */ public function testEncodersWithBCrypt() { $container = $this->getContainer('bcrypt_encoder'); @@ -458,6 +530,7 @@ public function testEncodersWithBCrypt() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User3' => [ 'algorithm' => 'md5', @@ -470,6 +543,7 @@ public function testEncodersWithBCrypt() 'memory_cost' => null, 'time_cost' => null, 'threads' => null, + 'migrate_from' => [], ], 'JMS\FooBundle\Entity\User4' => new Reference('security.encoder.foo'), 'JMS\FooBundle\Entity\User5' => [ @@ -481,8 +555,8 @@ public function testEncodersWithBCrypt() 'arguments' => [8, 102400, 15], ], 'JMS\FooBundle\Entity\User7' => [ - 'class' => 'Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder', - 'arguments' => [15], + 'class' => NativePasswordEncoder::class, + 'arguments' => [null, null, 15, \PASSWORD_BCRYPT], ], ]], $container->getDefinition('security.encoder_factory.generic')->getArguments()); } @@ -535,12 +609,10 @@ public function testCustomAccessDecisionManagerService() $this->assertSame('app.access_decision_manager', (string) $container->getAlias('security.access.decision_manager'), 'The custom access decision manager service is aliased'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage Invalid configuration for path "security.access_decision_manager": "strategy" and "service" cannot be used together. - */ public function testAccessDecisionManagerServiceAndStrategyCannotBeUsedAtTheSameTime() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid configuration for path "security.access_decision_manager": "strategy" and "service" cannot be used together.'); $this->getContainer('access_decision_manager_service_and_strategy'); } @@ -555,21 +627,17 @@ public function testAccessDecisionManagerOptionsAreNotOverriddenByImplicitStrate $this->assertFalse($accessDecisionManagerDefinition->getArgument(3)); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage Invalid firewall "main": user provider "undefined" not found. - */ public function testFirewallUndefinedUserProvider() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid firewall "main": user provider "undefined" not found.'); $this->getContainer('firewall_undefined_provider'); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage Invalid firewall "main": user provider "undefined" not found. - */ public function testFirewallListenerUndefinedProvider() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Invalid firewall "main": user provider "undefined" not found.'); $this->getContainer('listener_undefined_provider'); } @@ -598,9 +666,9 @@ public function testSimpleAuth() foreach (array_keys($arguments[1]->getValues()) as $contextId) { $contextDef = $container->getDefinition($contextId); $arguments = $contextDef->getArguments(); - $listeners[] = array_map('strval', $arguments['index_0']->getValues()); + $listeners[] = array_map('strval', $arguments[0]->getValues()); - $configDef = $container->getDefinition((string) $arguments['index_3']); + $configDef = $container->getDefinition((string) $arguments[3]); $configs[] = array_values($configDef->getArguments()); } @@ -649,6 +717,8 @@ protected function getContainer($file) $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); + $container->setParameter('request_listener.http_port', 80); + $container->setParameter('request_listener.https_port', 443); $security = new SecurityExtension(); $container->registerExtension($security); @@ -657,8 +727,8 @@ protected function getContainer($file) $bundle->build($container); // Attach all default factories $this->getLoader($container)->load($file); - $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); return $container; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php index 88543b7da95d9..ddac043692cf1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/argon2i_encoder.php @@ -1,6 +1,6 @@ <?php -$this->load('container1.php', $container); +$this->load('container1.php'); $container->loadFromExtension('security', [ 'encoders' => [ @@ -8,7 +8,6 @@ 'algorithm' => 'argon2i', 'memory_cost' => 256, 'time_cost' => 1, - 'threads' => 2, ], ], ]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php index 1afad79e4fca3..d4511aeb554c7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/bcrypt_encoder.php @@ -1,6 +1,6 @@ <?php -$this->load('container1.php', $container); +$this->load('container1.php'); $container->loadFromExtension('security', [ 'encoders' => [ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php new file mode 100644 index 0000000000000..c7ad9f02ab4f5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/migrating_encoder.php @@ -0,0 +1,14 @@ +<?php + +$this->load('container1.php'); + +$container->loadFromExtension('security', [ + 'encoders' => [ + 'JMS\FooBundle\Entity\User7' => [ + 'algorithm' => 'argon2i', + 'memory_cost' => 256, + 'time_cost' => 1, + 'migrate_from' => 'bcrypt', + ], + ], +]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml index 21b0c27443822..6a7c2a5041cdb 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/argon2i_encoder.xml @@ -10,7 +10,7 @@ </imports> <sec:config> - <sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory_cost="256" time_cost="1" threads="2" /> + <sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory_cost="256" time_cost="1" /> </sec:config> </container> diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml new file mode 100644 index 0000000000000..d820118075108 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/migrating_encoder.xml @@ -0,0 +1,18 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<container xmlns="http://symfony.com/schema/dic/services" + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xmlns:sec="http://symfony.com/schema/dic/security" + xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd"> + + <imports> + <import resource="container1.xml"/> + </imports> + + <sec:config> + <sec:encoder class="JMS\FooBundle\Entity\User7" algorithm="argon2i" memory-cost="256" time-cost="1"> + <sec:migrate-from>bcrypt</sec:migrate-from> + </sec:encoder> + </sec:config> + +</container> diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml index 6abd4d079893e..cadf8eb1e98d2 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/argon2i_encoder.yml @@ -7,4 +7,3 @@ security: algorithm: argon2i memory_cost: 256 time_cost: 1 - threads: 2 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml new file mode 100644 index 0000000000000..9eda61c18866f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/migrating_encoder.yml @@ -0,0 +1,10 @@ +imports: + - { resource: container1.yml } + +security: + encoders: + JMS\FooBundle\Entity\User7: + algorithm: argon2i + memory_cost: 256 + time_cost: 1 + migrate_from: bcrypt diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php index 88565a47cd9de..ffefe42ccb8d9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/MainConfigurationTest.php @@ -13,7 +13,9 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DependencyInjection\MainConfiguration; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\Config\Definition\Processor; +use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; class MainConfigurationTest extends TestCase { @@ -32,11 +34,9 @@ class MainConfigurationTest extends TestCase ], ]; - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - */ public function testNoConfigForProvider() { + $this->expectException(InvalidConfigurationException::class); $config = [ 'providers' => [ 'stub' => [], @@ -48,11 +48,9 @@ public function testNoConfigForProvider() $processor->processConfiguration($configuration, [$config]); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - */ public function testManyConfigForProvider() { + $this->expectException(InvalidConfigurationException::class); $config = [ 'providers' => [ 'stub' => [ @@ -116,4 +114,22 @@ public function testUserCheckers() $this->assertEquals('app.henk_checker', $processedConfig['firewalls']['stub']['user_checker']); } + + public function testConfigMergeWithAccessDecisionManager() + { + $config = [ + 'access_decision_manager' => [ + 'strategy' => AccessDecisionManager::STRATEGY_UNANIMOUS, + ], + ]; + $config = array_merge(static::$minimalConfig, $config); + + $config2 = []; + + $processor = new Processor(); + $configuration = new MainConfiguration([], []); + $processedConfig = $processor->processConfiguration($configuration, [$config, $config2]); + + $this->assertSame(AccessDecisionManager::STRATEGY_UNANIMOUS, $processedConfig['access_decision_manager']['strategy']); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php index 9eb9a08177700..c12e0c1e950bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Security\Factory; use PHPUnit\Framework\TestCase; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -19,7 +20,7 @@ class AbstractFactoryTest extends TestCase { public function testCreate() { - list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', [ + [$container, $authProviderId, $listenerId, $entryPointId] = $this->callFactory('foo', [ 'use_forward' => true, 'failure_path' => '/foo', 'success_handler' => 'custom_success_handler', @@ -61,7 +62,7 @@ public function testDefaultFailureHandler($serviceId, $defaultHandlerInjection) $options['failure_handler'] = $serviceId; } - list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); + [$container] = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); $definition = $container->getDefinition('abstract_listener.foo'); $arguments = $definition->getArguments(); @@ -99,7 +100,7 @@ public function testDefaultSuccessHandler($serviceId, $defaultHandlerInjection) $options['success_handler'] = $serviceId; } - list($container, $authProviderId, $listenerId, $entryPointId) = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); + [$container] = $this->callFactory('foo', $options, 'user_provider', 'entry_point'); $definition = $container->getDefinition('abstract_listener.foo'); $arguments = $definition->getArguments(); @@ -127,7 +128,7 @@ public function getSuccessHandlers() protected function callFactory($id, $config, $userProviderId, $defaultEntryPointId) { - $factory = $this->getMockForAbstractClass('Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AbstractFactory', []); + $factory = $this->getMockForAbstractClass(AbstractFactory::class); $factory ->expects($this->once()) @@ -150,7 +151,7 @@ protected function callFactory($id, $config, $userProviderId, $defaultEntryPoint $container->register('custom_success_handler'); $container->register('custom_failure_handler'); - list($authProviderId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); + [$authProviderId, $listenerId, $entryPointId] = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); return [$container, $authProviderId, $listenerId, $entryPointId]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php index 81db40412a30f..e5044a2bb92b6 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/GuardAuthenticationFactoryTest.php @@ -14,6 +14,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -37,11 +38,11 @@ public function testAddValidConfiguration(array $inputConfig, array $expectedCon } /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException * @dataProvider getInvalidConfigurationTests */ public function testAddInvalidConfiguration(array $inputConfig) { + $this->expectException(InvalidConfigurationException::class); $factory = new GuardAuthenticationFactory(); $nodeDefinition = new ArrayNodeDefinition('guard'); $factory->addConfiguration($nodeDefinition); @@ -103,7 +104,7 @@ public function testBasicCreate() 'authenticators' => ['authenticator123'], 'entry_point' => null, ]; - list($container, $entryPointId) = $this->executeCreate($config, null); + [$container, $entryPointId] = $this->executeCreate($config, null); $this->assertEquals('authenticator123', $entryPointId); $providerDefinition = $container->getDefinition('security.authentication.provider.guard.my_firewall'); @@ -126,15 +127,13 @@ public function testExistingDefaultEntryPointUsed() 'authenticators' => ['authenticator123'], 'entry_point' => null, ]; - list(, $entryPointId) = $this->executeCreate($config, 'some_default_entry_point'); + [, $entryPointId] = $this->executeCreate($config, 'some_default_entry_point'); $this->assertEquals('some_default_entry_point', $entryPointId); } - /** - * @expectedException \LogicException - */ public function testCannotOverrideDefaultEntryPoint() { + $this->expectException(\LogicException::class); // any existing default entry point is used $config = [ 'authenticators' => ['authenticator123'], @@ -143,11 +142,9 @@ public function testCannotOverrideDefaultEntryPoint() $this->executeCreate($config, 'some_default_entry_point'); } - /** - * @expectedException \LogicException - */ public function testMultipleAuthenticatorsRequiresEntryPoint() { + $this->expectException(\LogicException::class); // any existing default entry point is used $config = [ 'authenticators' => ['authenticator123', 'authenticatorABC'], @@ -163,7 +160,7 @@ public function testCreateWithEntryPoint() 'authenticators' => ['authenticator123', 'authenticatorABC'], 'entry_point' => 'authenticatorABC', ]; - list($container, $entryPointId) = $this->executeCreate($config, null); + [, $entryPointId] = $this->executeCreate($config, null); $this->assertEquals('authenticatorABC', $entryPointId); } @@ -176,7 +173,7 @@ private function executeCreate(array $config, $defaultEntryPointId) $userProviderId = 'my_user_provider'; $factory = new GuardAuthenticationFactory(); - list($providerId, $listenerId, $entryPointId) = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); + [, , $entryPointId] = $factory->create($container, $id, $config, $userProviderId, $defaultEntryPointId); return [$container, $entryPointId]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index 3145d035720fd..9d96cbe36b5b7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -16,6 +16,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\SecurityExtension; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\DependencyInjection\Fixtures\UserProvider\DummyProvider; +use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; use Symfony\Component\DependencyInjection\Argument\IteratorArgument; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Reference; @@ -24,12 +25,10 @@ class SecurityExtensionTest extends TestCase { - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage The check_path "/some_area/login_check" for login method "form_login" is not matched by the firewall pattern "/secured_area/.*". - */ public function testInvalidCheckPath() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('The check_path "/some_area/login_check" for login method "form_login" is not matched by the firewall pattern "/secured_area/.*".'); $container = $this->getRawContainer(); $container->loadFromExtension('security', [ @@ -50,12 +49,10 @@ public function testInvalidCheckPath() $container->compile(); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage No authentication listener registered for firewall "some_firewall" - */ public function testFirewallWithoutAuthenticationListener() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('No authentication listener registered for firewall "some_firewall"'); $container = $this->getRawContainer(); $container->loadFromExtension('security', [ @@ -73,12 +70,10 @@ public function testFirewallWithoutAuthenticationListener() $container->compile(); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage Unable to create definition for "security.user.provider.concrete.my_foo" user provider - */ public function testFirewallWithInvalidUserProvider() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Unable to create definition for "security.user.provider.concrete.my_foo" user provider'); $container = $this->getRawContainer(); $extension = $container->getExtension('security'); @@ -194,12 +189,10 @@ public function testPerListenerProvider() $this->addToAssertionCount(1); } - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage Not configuring explicitly the provider for the "http_basic" listener on "ambiguous" firewall is ambiguous as there is more than one registered provider. - */ public function testMissingProviderForListener() { + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('Not configuring explicitly the provider for the "http_basic" listener on "ambiguous" firewall is ambiguous as there is more than one registered provider.'); $container = $this->getRawContainer(); $container->loadFromExtension('security', [ 'providers' => [ @@ -218,7 +211,7 @@ public function testMissingProviderForListener() $container->compile(); } - public function testPerListenerProviderWithRememberMe() + public function testPerListenerProviderWithRememberMeAndAnonymous() { $container = $this->getRawContainer(); $container->loadFromExtension('security', [ @@ -231,6 +224,7 @@ public function testPerListenerProviderWithRememberMe() 'default' => [ 'form_login' => ['provider' => 'second'], 'remember_me' => ['secret' => 'baz'], + 'anonymous' => true, ], ], ]); @@ -397,6 +391,80 @@ public function sessionConfigurationProvider() ]; } + public function testSwitchUserWithSeveralDefinedProvidersButNoFirewallRootProviderConfigured() + { + $container = $this->getRawContainer(); + $container->loadFromExtension('security', [ + 'providers' => [ + 'first' => ['id' => 'foo'], + 'second' => ['id' => 'bar'], + ], + + 'firewalls' => [ + 'foobar' => [ + 'switch_user' => [ + 'provider' => 'second', + ], + 'anonymous' => true, + ], + ], + ]); + + $container->compile(); + + $this->assertEquals(new Reference('security.user.provider.concrete.second'), $container->getDefinition('security.authentication.switchuser_listener.foobar')->getArgument(1)); + } + + public function testInvalidAccessControlWithEmptyRow() + { + $container = $this->getRawContainer(); + + $container->loadFromExtension('security', [ + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + [], + ['path' => '/admin', 'roles' => 'ROLE_ADMIN'], + ], + ]); + + $this->expectException(InvalidConfigurationException::class); + $this->expectExceptionMessage('One or more access control items are empty. Did you accidentally add lines only containing a "-" under "security.access_control"?'); + $container->compile(); + } + + public function testValidAccessControlWithEmptyRow() + { + $container = $this->getRawContainer(); + + $container->loadFromExtension('security', [ + 'providers' => [ + 'default' => ['id' => 'foo'], + ], + 'firewalls' => [ + 'some_firewall' => [ + 'pattern' => '/.*', + 'http_basic' => [], + ], + ], + 'access_control' => [ + ['path' => '^/login'], + ['path' => '^/', 'roles' => 'ROLE_USER'], + ], + ]); + + $container->compile(); + + $this->assertTrue(true, 'extension throws an InvalidConfigurationException if there is one more more empty access control items'); + } + protected function getRawContainer() { $container = new ContainerBuilder(); @@ -410,6 +478,7 @@ protected function getRawContainer() $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); return $container; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php b/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php new file mode 100644 index 0000000000000..1102a4f3b0d16 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Fixtures/TokenInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace Symfony\Bundle\SecurityBundle\Tests\Fixtures; + +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface as BaseTokenInterface; + +interface TokenInterface extends BaseTokenInterface +{ + public function __serialize(): array; + public function __unserialize(array $data): void; +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AbstractWebTestCase.php similarity index 72% rename from src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/AbstractWebTestCase.php index 9bcbc0532481d..e677907eebe0a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/WebTestCase.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AbstractWebTestCase.php @@ -13,8 +13,9 @@ use Symfony\Bundle\FrameworkBundle\Test\WebTestCase as BaseWebTestCase; use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\HttpKernel\KernelInterface; -class WebTestCase extends BaseWebTestCase +abstract class AbstractWebTestCase extends BaseWebTestCase { public static function assertRedirect($response, $location) { @@ -22,12 +23,12 @@ public static function assertRedirect($response, $location) self::assertEquals('http://localhost'.$location, $response->headers->get('Location')); } - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { static::deleteTmpDir(); } - public static function tearDownAfterClass() + public static function tearDownAfterClass(): void { static::deleteTmpDir(); } @@ -42,14 +43,14 @@ protected static function deleteTmpDir() $fs->remove($dir); } - protected static function getKernelClass() + protected static function getKernelClass(): string { require_once __DIR__.'/app/AppKernel.php'; return 'Symfony\Bundle\SecurityBundle\Tests\Functional\app\AppKernel'; } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { $class = self::getKernelClass(); @@ -60,14 +61,14 @@ protected static function createKernel(array $options = []) return new $class( static::getVarDir(), $options['test_case'], - isset($options['root_config']) ? $options['root_config'] : 'config.yml', - isset($options['environment']) ? $options['environment'] : strtolower(static::getVarDir().$options['test_case']), - isset($options['debug']) ? $options['debug'] : false + $options['root_config'] ?? 'config.yml', + $options['environment'] ?? strtolower(static::getVarDir().$options['test_case']), + $options['debug'] ?? false ); } protected static function getVarDir() { - return 'SB'.substr(strrchr(\get_called_class(), '\\'), 1); + return 'SB'.substr(strrchr(static::class, '\\'), 1); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php new file mode 100644 index 0000000000000..fdee9bce9b06a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AnonymousTest.php @@ -0,0 +1,24 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class AnonymousTest extends AbstractWebTestCase +{ + public function testAnonymous() + { + $client = $this->createClient(['test_case' => 'Anonymous', 'root_config' => 'config.yml']); + + $client->request('GET', '/'); + + $this->assertSame(401, $client->getResponse()->getStatusCode()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php index 2a31f2a27a5f2..dcfd6f29e8fea 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AuthenticationCommencingTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class AuthenticationCommencingTest extends WebTestCase +class AuthenticationCommencingTest extends AbstractWebTestCase { public function testAuthenticationIsCommencingIfAccessDeniedExceptionIsWrapped() { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php index 31d296e48ad89..9d17b13a10b6f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/AutowiringTypesTest.php @@ -11,10 +11,11 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Security\Core\Authorization\AccessDecisionManager; use Symfony\Component\Security\Core\Authorization\TraceableAccessDecisionManager; -class AutowiringTypesTest extends WebTestCase +class AutowiringTypesTest extends AbstractWebTestCase { public function testAccessDecisionManagerAutowiring() { @@ -29,7 +30,7 @@ public function testAccessDecisionManagerAutowiring() $this->assertInstanceOf(Trac 10000 eableAccessDecisionManager::class, $autowiredServices->getAccessDecisionManager(), 'The debug.security.access.decision_manager service should be injected in non-debug mode'); } - protected static function createKernel(array $options = []) + protected static function createKernel(array $options = []): KernelInterface { return parent::createKernel(['test_case' => 'AutowiringTypes'] + $options); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php new file mode 100644 index 0000000000000..5069fa9cc7fa9 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/AnonymousBundle/AppCustomAuthenticator.php @@ -0,0 +1,57 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + +class AppCustomAuthenticator extends AbstractGuardAuthenticator +{ + public function supports(Request $request) + { + return false; + } + + public function getCredentials(Request $request) + { + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + } + + public function checkCredentials($credentials, UserInterface $user) + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + } + + public function start(Request $request, AuthenticationException $authException = null) + { + return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); + } + + public function supportsRememberMe() + { + } +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/DependencyInjection/ExtensionLoadedExtension.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/DependencyInjection/EventExtension.php similarity index 52% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/DependencyInjection/ExtensionLoadedExtension.php rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/DependencyInjection/EventExtension.php index b43bc665a843e..34159fd09b0a8 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/DependencyInjection/ExtensionLoadedExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/DependencyInjection/EventExtension.php @@ -9,14 +9,18 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionLoadedBundle\DependencyInjection; +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\DependencyInjection; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventSubscriber\TestSubscriber; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Extension\Extension; -class ExtensionLoadedExtension extends Extension +final class EventExtension extends Extension { - public function load(array $configs, ContainerBuilder $container) + public function load(array $configs, ContainerBuilder $container): void { + $container->register('test_subscriber', TestSubscriber::class) + ->setPublic(true) + ->addTag('kernel.event_subscriber'); } } diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/ExtensionLoadedBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventBundle.php similarity index 70% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/ExtensionLoadedBundle.php rename to src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventBundle.php index 3af81cb07396d..5c0ece872e56f 100644 --- a/src/Symfony/Component/HttpKernel/Tests/Fixtures/ExtensionLoadedBundle/ExtensionLoadedBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventBundle.php @@ -9,10 +9,10 @@ * file that was distributed with this source code. */ -namespace Symfony\Component\HttpKernel\Tests\Fixtures\ExtensionLoadedBundle; +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle; use Symfony\Component\HttpKernel\Bundle\Bundle; -class ExtensionLoadedBundle extends Bundle +final class EventBundle extends Bundle { } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventSubscriber/TestSubscriber.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventSubscriber/TestSubscriber.php new file mode 100644 index 0000000000000..0a907008ec411 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/EventBundle/EventSubscriber/TestSubscriber.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventSubscriber; + +use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\SwitchUserEvent; + +final class TestSubscriber implements EventSubscriberInterface +{ + public $calledMethods = []; + + public static function getSubscribedEvents(): array + { + return [ + AuthenticationSuccessEvent::class => 'onAuthenticationSuccess', + AuthenticationFailureEvent::class => 'onAuthenticationFailure', + InteractiveLoginEvent::class => 'onInteractiveLogin', + SwitchUserEvent::class => 'onSwitchUser', + ]; + } + + public function __call(string $name, array $arguments) + { + $this->calledMethods[$name] = ($this->calledMethods[$name] ?? 0) + 1; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FirewallEntryPointBundle/Security/EntryPointStub.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FirewallEntryPointBundle/Security/EntryPointStub.php index e1d3280570d88..56552b99c7983 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FirewallEntryPointBundle/Security/EntryPointStub.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FirewallEntryPointBundle/Security/EntryPointStub.php @@ -18,9 +18,9 @@ class EntryPointStub implements AuthenticationEntryPointInterface { - const RESPONSE_TEXT = '2be8e651259189d841a19eecdf37e771e2431741'; + public const RESPONSE_TEXT = '2be8e651259189d841a19eecdf37e771e2431741'; - public function start(Request $request, AuthenticationException $authException = null) + public function start(Request $request, AuthenticationException $authException = null): Response { return new Response(self::RESPONSE_TEXT); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php index 269827e2df5f2..cf0e1150aff9a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Controller/LocalizedController.php @@ -59,6 +59,6 @@ public function profileAction() public function homepageAction() { - return new Response('<html><body>Homepage</body></html>'); + return (new Response('<html><body>Homepage</body></html>'))->setPublic(); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php index f8f1c450d3963..afb4648d36c69 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/FormLoginBundle/Security/LocalizedFormFailureHandler.php @@ -13,6 +13,7 @@ use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; @@ -27,7 +28,7 @@ public function __construct(RouterInterface $router) $this->router = $router; } - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { return new RedirectResponse($this->router->generate('localized_login_path', [], UrlGeneratorInterface::ABSOLUTE_URL)); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php new file mode 100644 index 0000000000000..22d378835e4c0 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AppCustomAuthenticator.php @@ -0,0 +1,59 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; +use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; + +class AppCustomAuthenticator extends AbstractGuardAuthenticator +{ + public function supports(Request $request) + { + return '/manual_login' !== $request->getPathInfo() && '/profile' !== $request->getPathInfo(); + } + + public function getCredentials(Request $request) + { + throw new AuthenticationException('This should be hit'); + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + } + + public function checkCredentials($credentials, UserInterface $user) + { + } + + public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + { + return new Response('', 418); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + } + + public function start(Request $request, AuthenticationException $authException = null) + { + return new Response($authException->getMessage(), Response::HTTP_UNAUTHORIZED); + } + + public function supportsRememberMe() + { + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php new file mode 100644 index 0000000000000..9833d05513833 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/GuardedBundle/AuthenticationController.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle; + +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Guard\GuardAuthenticatorHandler; +use Symfony\Component\Security\Guard\Token\PostAuthenticationGuardToken; + +class AuthenticationController +{ + public function manualLoginAction(GuardAuthenticatorHandler $guardAuthenticatorHandler, Request $request) + { + $guardAuthenticatorHandler->authenticateWithToken(new PostAuthenticationGuardToken(new User('Jane', 'test', ['ROLE_USER']), 'secure', ['ROLE_USER']), $request, 'secure'); + + return new Response('Logged in.'); + } + + public function profileAction(UserInterface $user = null) + { + if (null === $user) { + return new Response('Not logged in.'); + } + + return new Response('Username: '.$user->getUsername()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationFailureHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationFailureHandler.php index 737c5a5abec00..26ba36e3d3dd7 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationFailureHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationFailureHandler.php @@ -13,12 +13,13 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Http\Authentication\AuthenticationFailureHandlerInterface; class JsonAuthenticationFailureHandler implements AuthenticationFailureHandlerInterface { - public function onAuthenticationFailure(Request $request, AuthenticationException $exception) + public function onAuthenticationFailure(Request $request, AuthenticationException $exception): Response { return new JsonResponse(['message' => 'Something went wrong'], 500); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php index 0390eb8e35eba..a0300d4d78387 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/JsonLoginBundle/Security/Http/JsonAuthenticationSuccessHandler.php @@ -13,12 +13,13 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface; class JsonAuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface { - public function onAuthenticationSuccess(Request $request, TokenInterface $token) + public function onAuthenticationSuccess(Request $request, TokenInterface $token): Response { return new JsonResponse(['message' => sprintf('Good game @%s!', $token->getUsername())]); } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Controller/AdminController.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Controller/AdminController.php new file mode 100644 index 0000000000000..f9e73b8970657 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Controller/AdminController.php @@ -0,0 +1,26 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Controller; + +use Symfony\Component\DependencyInjection\ContainerAwareInterface; +use Symfony\Component\DependencyInjection\ContainerAwareTrait; +use Symfony\Component\HttpFoundation\Response; + +class AdminController implements ContainerAwareInterface +{ + use ContainerAwareTrait; + + public function indexAction() + { + return new Response('admin'); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Resources/config/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Resources/config/routing.yml new file mode 100644 index 0000000000000..21788d4d890b4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Resources/config/routing.yml @@ -0,0 +1,3 @@ +admin: + path: /admin + defaults: { _controller: \Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Controller\AdminController::indexAction } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/SecuredPageBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/SecuredPageBundle.php new file mode 100644 index 0000000000000..92d743d2c754e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/SecuredPageBundle.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class SecuredPageBundle extends Bundle +{ +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php new file mode 100644 index 0000000000000..d2ad58e2bf575 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/SecuredPageBundle/Security/Core/User/ArrayUserProvider.php @@ -0,0 +1,71 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User; + +use Symfony\Bundle\SecurityBundle\Tests\Functional\UserWithoutEquatable; +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +class ArrayUserProvider implements UserProviderInterface +{ + /** @var UserInterface[] */ + private $users = []; + + public function addUser(UserInterface $user) + { + $this->users[$user->getUsername()] = $user; + } + + public function setUser($username, UserInterface $user) + { + $this->users[$username] = $user; + } + + public function getUser($username) + { + return $this->users[$username]; + } + + public function loadUserByUsername($username) + { + $user = $this->getUser($username); + + if (null === $user) { + $e = new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + $e->setUsername($username); + + throw $e; + } + + return $user; + } + + public function refreshUser(UserInterface $user) + { + if (!$user instanceof UserInterface) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + } + + $storedUser = $this->getUser($user->getUsername()); + $class = \get_class($storedUser); + + return new $class($storedUser->getUsername(), $storedUser->getPassword(), $storedUser->getRoles(), $storedUser->isEnabled(), $storedUser->isAccountNonExpired(), $storedUser->isCredentialsNonExpired() && $storedUser->getPassword() === $user->getPassword(), $storedUser->isAccountNonLocked()); + } + + public function supportsClass($class) + { + return User::class === $class || UserWithoutEquatable::class === $class; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php new file mode 100644 index 0000000000000..5197a16195e2e --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/Bundle/TestBundle.php @@ -0,0 +1,38 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle; + +use Symfony\Component\DependencyInjection\Compiler\CheckTypeDeclarationsPass; +use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; +use Symfony\Component\DependencyInjection\Compiler\PassConfig; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class TestBundle extends Bundle +{ + public function build(ContainerBuilder $container) + { + $container->setParameter('container.build_hash', 'test_bundle'); + $container->setParameter('container.build_time', time()); + $container->setParameter('container.build_id', 'test_bundle'); + + $container->addCompilerPass(new class() implements CompilerPassInterface { + public function process(ContainerBuilder $container) + { + $container->removeDefinition('twig.controller.exception'); + $container->removeDefinition('twig.controller.preview_error'); + } + }); + + $container->addCompilerPass(new CheckTypeDeclarationsPass(true), PassConfig::TYPE_AFTER_REMOVING, -100); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php new file mode 100644 index 0000000000000..51f56c220d33c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/ClearRememberMeTest.php @@ -0,0 +1,77 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Security\Core\User\InMemoryUserProvider; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Core\User\UserProviderInterface; + +class ClearRememberMeTest extends AbstractWebTestCase +{ + public function testUserChangeClearsCookie() + { + $client = $this->createClient(['test_case' => 'ClearRememberMe', 'root_config' => 'config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'johannes', + '_password' => 'test', + ]); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $cookieJar = $client->getCookieJar(); + $this->assertNotNull($cookieJar->get('REMEMBERME')); + + $client->request('GET', '/foo'); + $this->assertRedirect($client->getResponse(), '/login'); + $this->assertNull($cookieJar->get('REMEMBERME')); + } +} + +class RememberMeFooController +{ + public function __invoke(UserInterface $user) + { + return new Response($user->getUsername()); + } +} + +class RememberMeUserProvider implements UserProviderInterface +{ + private $inner; + + public function __construct(InMemoryUserProvider $inner) + { + $this->inner = $inner; + } + + public function loadUserByUsername($username) + { + return $this->inner->loadUserByUsername($username); + } + + public function refreshUser(UserInterface $user) + { + $user = $this->inner->refreshUser($user); + + $alterUser = \Closure::bind(function (User $user) { $user->password = 'foo'; }, null, User::class); + $alterUser($user); + + return $user; + } + + public function supportsClass($class) + { + return $this->inner->supportsClass($class); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php index 98b52a5f05058..08ea67a6416fa 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/CsrfFormLoginTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class CsrfFormLoginTest extends WebTestCase +class CsrfFormLoginTest extends AbstractWebTestCase { /** * @dataProvider getConfigs @@ -19,24 +19,26 @@ class CsrfFormLoginTest extends WebTestCase public function testFormLoginAndLogoutWithCsrfTokens($config) { $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['user_login[username]'] = 'johannes'; $form['user_login[password]'] = 'test'; $client->submit($form); + $this->assertFalse(static::$container->get('security.csrf.token_storage')->hasToken('foo')); + $this->assertRedirect($client->getResponse(), '/profile'); $crawler = $client->followRedirect(); - $text = $crawler->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/profile".', $text); + $text = $crawler->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/profile".', $text); $logoutLinks = $crawler->selectLink('Log out')->links(); $this->assertCount(2, $logoutLinks); - $this->assertContains('_csrf_token=', $logoutLinks[0]->getUri()); - $this->assertSame($logoutLinks[0]->getUri(), $logoutLinks[1]->getUri()); + $this->assertStringContainsString('_csrf_token=', $logoutLinks[0]->getUri()); $client->click($logoutLinks[0]); @@ -49,15 +51,18 @@ public function testFormLoginAndLogoutWithCsrfTokens($config) public function testFormLoginWithInvalidCsrfToken($config) { $client = $this->createClient(['test_case' => 'CsrfFormLogin', 'root_config' => $config]); + static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); $form = $client->request('GET', '/login')->selectButton('login')->form(); $form['user_login[_token]'] = ''; $client->submit($form); + $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo')); + $this->assertRedirect($client->getResponse(), '/login'); - $text = $client->followRedirect()->text(); - $this->assertContains('Invalid CSRF token.', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Invalid CSRF token.', $text); } /** @@ -75,9 +80,9 @@ public function testFormLoginWithCustomTargetPath($config) $this->assertRedirect($client->getResponse(), '/foo'); - $text = $client->followRedirect()->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/foo".', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/foo".', $text); } /** @@ -96,9 +101,9 @@ public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) $client->submit($form); $this->assertRedirect($client->getResponse(), '/protected-resource'); - $text = $client->followRedirect()->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/protected-resource".', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/protected-resource".', $text); } public function getConfigs() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php new file mode 100644 index 0000000000000..d1f8860e4b1eb --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/EventAliasTest.php @@ -0,0 +1,48 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Bundle\SecurityBundle\Tests\Fixtures\TokenInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Security\Core\AuthenticationEvents; +use Symfony\Component\Security\Core\Event\AuthenticationFailureEvent; +use Symfony\Component\Security\Core\Event\AuthenticationSuccessEvent; +use Symfony\Component\Security\Core\Exception\AuthenticationException; +use Symfony\Component\Security\Core\User\UserInterface; +use Symfony\Component\Security\Http\Event\InteractiveLoginEvent; +use Symfony\Component\Security\Http\Event\SwitchUserEvent; +use Symfony\Component\Security\Http\SecurityEvents; + +final class EventAliasTest extends AbstractWebTestCase +{ + public function testAliasedEvents() + { + $client = $this->createClient(['test_case' => 'AliasedEvents', 'root_config' => 'config.yml']); + $container = $client->getContainer(); + $dispatcher = $container->get('event_dispatcher'); + + $dispatcher->dispatch(new AuthenticationSuccessEvent($this->createMock(TokenInterface::class)), AuthenticationEvents::AUTHENTICATION_SUCCESS); + $dispatcher->dispatch(new AuthenticationFailureEvent($this->createMock(TokenInterface::class), new AuthenticationException()), AuthenticationEvents::AUTHENTICATION_FAILURE); + $dispatcher->dispatch(new InteractiveLoginEvent($this->createMock(Request::class), $this->createMock(TokenInterface::class)), SecurityEvents::INTERACTIVE_LOGIN); + $dispatcher->dispatch(new SwitchUserEvent($this->createMock(Request::class), $this->createMock(UserInterface::class), $this->createMock(TokenInterface::class)), SecurityEvents::SWITCH_USER); + + $this->assertEquals( + [ + 'onAuthenticationSuccess' => 1, + 'onAuthenticationFailure' => 1, + 'onInteractiveLogin' => 1, + 'onSwitchUser' => 1, + ], + $container->get('test_subscriber')->calledMethods + ); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php index 8afedc42e44d3..77011409cfaa4 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FirewallEntryPointTest.php @@ -13,7 +13,7 @@ use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\Security\EntryPointStub; -class FirewallEntryPointTest extends WebTestCase +class FirewallEntryPointTest extends AbstractWebTestCase { public function testItUsesTheConfiguredEntryPointWhenUsingUnknownCredentials() { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php index ec1722188af25..641ef0e519a1d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/FormLoginTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class FormLoginTest extends WebTestCase +class FormLoginTest extends AbstractWebTestCase { /** * @dataProvider getConfigs @@ -27,9 +27,9 @@ public function testFormLogin($config) $this->assertRedirect($client->getResponse(), '/profile'); - $text = $client->followRedirect()->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/profile".', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/profile".', $text); } /** @@ -47,10 +47,10 @@ public function testFormLogout($config) $this->assertRedirect($client->getResponse(), '/profile'); $crawler = $client->followRedirect(); - $text = $crawler->text(); + $text = $crawler->text(null, true); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/profile".', $text); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/profile".', $text); $logoutLinks = $crawler->selectLink('Log out')->links(); $this->assertCount(6, $logoutLinks); @@ -80,9 +80,9 @@ public function testFormLoginWithCustomTargetPath($config) $this->assertRedirect($client->getResponse(), '/foo'); - $text = $client->followRedirect()->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/foo".', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/foo".', $text); } /** @@ -101,9 +101,9 @@ public function testFormLoginRedirectsToProtectedResourceAfterLogin($config) $client->submit($form); $this->assertRedirect($client->getResponse(), '/protected_resource'); - $text = $client->followRedirect()->text(); - $this->assertContains('Hello johannes!', $text); - $this->assertContains('You\'re browsing to path "/protected_resource".', $text); + $text = $client->followRedirect()->text(null, true); + $this->assertStringContainsString('Hello johannes!', $text); + $this->assertStringContainsString('You\'re browsing to path "/protected_resource".', $text); } public function getConfigs() diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php new file mode 100644 index 0000000000000..83cd4118d76e4 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/GuardedTest.php @@ -0,0 +1,34 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +class GuardedTest extends AbstractWebTestCase +{ + public function testGuarded() + { + $client = $this->createClient(['test_case' => 'Guarded', 'root_config' => 'config.yml']); + + $client->request('GET', '/'); + + $this->assertSame(418, $client->getResponse()->getStatusCode()); + } + + public function testManualLogin() + { + $client = $this->createClient(['debug' => true, 'test_case' => 'Guarded', 'root_config' => 'config.yml']); + + $client->request('GET', '/manual_login'); + $client->request('GET', '/profile'); + + $this->assertSame('Username: Jane', $client->getResponse()->getContent()); + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php index 6b7dca4b422f2..583e153695fed 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginLdapTest.php @@ -13,7 +13,7 @@ use Symfony\Component\HttpKernel\Kernel; -class JsonLoginLdapTest extends WebTestCase +class JsonLoginLdapTest extends AbstractWebTestCase { public function testKernelBoot() { diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php index c7e9e2aab71b1..a69f5e591d1fa 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/JsonLoginTest.php @@ -16,7 +16,7 @@ /** * @author KΓ©vin Dunglas <dunglas@gmail.com> */ -class JsonLoginTest extends WebTestCase +class JsonLoginTest extends AbstractWebTestCase { public function testDefaultJsonLoginSuccess() { @@ -70,6 +70,6 @@ public function testDefaultJsonLoginBadRequest() $this->assertSame(400, $response->getStatusCode()); $this->assertSame('application/json', $response->headers->get('Content-Type')); - $this->assertArraySubset(['error' => ['code' => 400, 'message' => 'Bad Request']], json_decode($response->getContent(), true)); + $this->assertSame(['type' => 'https://tools.ietf.org/html/rfc2616#section-10', 'title' => 'An error occurred', 'status' => 400, 'detail' => 'Bad Request'], json_decode($response->getContent(), true)); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php index c874ada34a4e3..2a45fc00de38f 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LocalizedRoutesAsPathTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class LocalizedRoutesAsPathTest extends WebTestCase +class LocalizedRoutesAsPathTest extends AbstractWebTestCase { /** * @dataProvider getLocales diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php index ef417923d2df0..465027f42f0c8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/LogoutTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class LogoutTest extends WebTestCase +class LogoutTest extends AbstractWebTestCase { public function testSessionLessRememberMeLogout() { @@ -36,15 +36,13 @@ public function testSessionLessRememberMeLogout() public function testCsrfTokensAreClearedOnLogout() { $client = $this->createClient(['test_case' => 'LogoutWithoutSessionInvalidation', 'root_config' => 'config.yml']); - static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); $client->request('POST', '/login', [ '_username' => 'johannes', '_password' => 'test', ]); - $this->assertTrue(static::$container->get('security.csrf.token_storage')->hasToken('foo')); - $this->assertSame('bar', static::$container->get('security.csrf.token_storage')->getToken('foo')); + static::$container->get('security.csrf.token_storage')->setToken('foo', 'bar'); $client->request('GET', '/logout'); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php index 378ff26b381e4..6c8ba6482e45b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/MissingUserProviderTest.php @@ -11,19 +11,20 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class MissingUserProviderTest extends WebTestCase +class MissingUserProviderTest extends AbstractWebTestCase { - /** - * @expectedException \Symfony\Component\Config\Definition\Exception\InvalidConfigurationException - * @expectedExceptionMessage "default" firewall requires a user provider but none was defined. - */ public function testUserProviderIsNeeded() { - $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml']); + $client = $this->createClient(['test_case' => 'MissingUserProvider', 'root_config' => 'config.yml', 'debug' => true]); $client->request('GET', '/', [], [], [ 'PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word', ]); + + $response = $client->getResponse(); + $this->assertSame(500, $response->getStatusCode()); + $this->assertStringContainsString('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException', $response->getContent()); + $this->assertStringContainsString('"default" firewall requires a user provider but none was defined', html_entity_decode($response->getContent())); } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php new file mode 100644 index 0000000000000..e4a1f0dd6cc4a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/RememberMeCookieTest.php @@ -0,0 +1,42 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\SecurityBundle\Tests\Functional; + +use Symfony\Component\HttpFoundation\ResponseHeaderBag; + +class RememberMeCookieTest extends AbstractWebTestCase +{ + /** @dataProvider getSessionRememberMeSecureCookieFlagAutoHttpsMap */ + public function testSessionRememberMeSecureCookieFlagAuto($https, $expectedSecureFlag) + { + $client = $this->createClient(['test_case' => 'RememberMeCookie', 'root_config' => 'config.yml']); + + $client->request('POST', '/login', [ + '_username' => 'test', + '_password' => 'test', + ], [], [ + 'HTTPS' => (int) $https, + ]); + + $cookies = $client->getResponse()->headers->getCookies(ResponseHeaderBag::COOKIES_ARRAY); + + $this->assertEquals($expectedSecureFlag, $cookies['']['/']['REMEMBERME']->isSecure()); + } + + public function getSessionRememberMeSecureCookieFlagAutoHttpsMap() + { + return [ + [true, true], + [false, false], + ]; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php index 3c5bf5a719e8b..0303f1b4eeff9 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityRoutingIntegrationTest.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; -class SecurityRoutingIntegrationTest extends WebTestCase +class SecurityRoutingIntegrationTest extends AbstractWebTestCase { /** * @dataProvider getConfigs @@ -129,6 +129,16 @@ public function testInvalidIpsInAccessControl() $client->request('GET', '/unprotected_resource'); } + public function testPublicHomepage() + { + $client = $this->createClient(['test_case' => 'StandardFormLogin', 'root_config' => 'config.yml']); + $client->request('GET', '/en/'); + + $this->assertEquals(200, $client->getResponse()->getStatusCode(), (string) $client->getResponse()); + $this->assertTrue($client->getResponse()->headers->getCacheControlDirective('public')); + $this->assertSame(0, self::$container->get('session')->getUsageIndex()); + } + private function assertAllowed($client, $path) { $client->request('GET', $path); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php index ff687d0792716..1f41e2646d1af 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SecurityTest.php @@ -11,10 +11,12 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Security\Core\User\UserInterface; -class SecurityTest extends WebTestCase +class SecurityTest extends AbstractWebTestCase { public function testServiceIsFunctional() { @@ -31,4 +33,149 @@ public function testServiceIsFunctional() $this->assertTrue($security->isGranted('ROLE_USER')); $this->assertSame($token, $security->getToken()); } + + public function userWillBeMarkedAsChangedIfRolesHasChangedProvider() + { + return [ + [ + new User('user1', 'test', ['ROLE_ADMIN']), + new User('user1', 'test', ['ROLE_USER']), + ], + [ + new UserWithoutEquatable('user1', 'test', ['ROLE_ADMIN']), + new UserWithoutEquatable('user1', 'test', ['ROLE_USER']), + ], + ]; + } + + /** + * @dataProvider userWillBeMarkedAsChangedIfRolesHasChangedProvider + */ + public function testUserWillBeMarkedAsChangedIfRolesHasChanged(UserInterface $userWithAdminRole, UserInterface $userWithoutAdminRole) + { + $client = $this->createClient(['test_case' => 'AbstractTokenCompareRoles', 'root_config' => 'config.yml']); + $client->disableReboot(); + + /** @var ArrayUserProvider $userProvider */ + $userProvider = static::$kernel->getContainer()->get('security.user.provider.array'); + $userProvider->addUser($userWithAdminRole); + + $client->request('POST', '/login', [ + '_username' => 'user1', + '_password' => 'test', + ]); + + // user1 has ROLE_ADMIN and can visit secure page + $client->request('GET', '/admin'); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + + // updating user provider with same user but revoked ROLE_ADMIN from user1 + $userProvider->setUser('user1', $userWithoutAdminRole); + + // user1 has lost ROLE_ADMIN and MUST be redirected away from secure page + $client->request('GET', '/admin'); + $this->assertEquals(302, $client->getResponse()->getStatusCode()); + } +} + +final class UserWithoutEquatable implements UserInterface +{ + private $username; + private $password; + private $enabled; + private $accountNonExpired; + private $credentialsNonExpired; + private $accountNonLocked; + private $roles; + + public function __construct(?string $username, ?string $password, array $roles = [], bool $enabled = true, bool $userNonExpired = true, bool $credentialsNonExpired = true, bool $userNonLocked = true) + { + if ('' === $username || null === $username) { + throw new \InvalidArgumentException('The username cannot be empty.'); + } + + $this->username = $username; + $this->password = $password; + $this->enabled = $enabled; + $this->accountNonExpired = $userNonExpired; + $this->credentialsNonExpired = $credentialsNonExpired; + $this->accountNonLocked = $userNonLocked; + $this->roles = $roles; + } + + public function __toString() + { + return $this->getUsername(); + } + + /** + * {@inheritdoc} + */ + public function getRoles() + { + return $this->roles; + } + + /** + * {@inheritdoc} + */ + public function getPassword() + { + return $this->password; + } + + /** + * {@inheritdoc} + */ + public function getSalt() + { + return null; + } + + /** + * {@inheritdoc} + */ + public function getUsername() + { + return $this->username; + } + + /** + * {@inheritdoc} + */ + public function isAccountNonExpired() + { + return $this->accountNonExpired; + } + + /** + * {@inheritdoc} + */ + public function isAccountNonLocked() + { + return $this->accountNonLocked; + } + + /** + * {@inheritdoc} + */ + public function isCredentialsNonExpired() + { + return $this->credentialsNonExpired; + } + + /** + * {@inheritdoc} + */ + public function isEnabled() + { + return $this->enabled; + } + + /** + * {@inheritdoc} + */ + public function eraseCredentials() + { + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php index ddbfd629c8fb7..183b1ad8c4ef8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/SwitchUserTest.php @@ -14,7 +14,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\Security\Http\Firewall\SwitchUserListener; -class SwitchUserTest extends WebTestCase +class SwitchUserTest extends AbstractWebTestCase { /** * @dataProvider getTestParameters @@ -29,15 +29,15 @@ public function testSwitchUser($originalUser, $targetUser, $expectedUser, $expec $this->assertEquals($expectedUser, $client->getProfile()->getCollector('security')->getUser()); } - public function testSwitchedUserCannotSwitchToOther() + public function testSwitchedUserCanSwitchToOther() { $client = $this->createAuthenticatedClient('user_can_switch'); $client->request('GET', '/profile?_switch_user=user_cannot_switch_1'); $client->request('GET', '/profile?_switch_user=user_cannot_switch_2'); - $this->assertEquals(500, $client->getResponse()->getStatusCode()); - $this->assertEquals('user_cannot_switch_1', $client->getProfile()->getCollector('security')->getUser()); + $this->assertEquals(200, $client->getResponse()->getStatusCode()); + $this->assertEquals('user_cannot_switch_2', $client->getProfile()->getCollector('security')->getUser()); } public function testSwitchedUserExit() @@ -68,7 +68,7 @@ public function getTestParameters() return [ 'unauthorized_user_cannot_switch' => ['user_cannot_switch_1', 'user_cannot_switch_1', 'user_cannot_switch_1', 403], 'authorized_user_can_switch' => ['user_can_switch', 'user_cannot_switch_1', 'user_cannot_switch_1', 200], - 'authorized_user_cannot_switch_to_non_existent' => ['user_can_switch', 'user_does_not_exist', 'user_can_switch', 500], + 'authorized_user_cannot_switch_to_non_existent' => ['user_can_switch', 'user_does_not_exist', 'user_can_switch', 403], 'authorized_user_can_switch_to_himself' => ['user_can_switch', 'user_can_switch', 'user_can_switch', 200], ]; } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index 7e90562246228..c2591b7f7f219 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -15,8 +15,6 @@ use Symfony\Bundle\SecurityBundle\Command\UserPasswordEncoderCommand; use Symfony\Component\Console\Application as ConsoleApplication; use Symfony\Component\Console\Tester\CommandTester; -use Symfony\Component\Security\Core\Encoder\Argon2iPasswordEncoder; -use Symfony\Component\Security\Core\Encoder\BCryptPasswordEncoder; use Symfony\Component\Security\Core\Encoder\EncoderFactoryInterface; use Symfony\Component\Security\Core\Encoder\NativePasswordEncoder; use Symfony\Component\Security\Core\Encoder\Pbkdf2PasswordEncoder; @@ -27,10 +25,11 @@ * * @author Sarah Khalil <mkhalil.sarah@gmail.com> */ -class UserPasswordEncoderCommandTest extends WebTestCase +class UserPasswordEncoderCommandTest extends AbstractWebTestCase { /** @var CommandTester */ private $passwordEncoderCommandTester; + private $colSize; public function testEncodePasswordEmptySalt() { @@ -40,7 +39,7 @@ public function testEncodePasswordEmptySalt() 'user-class' => 'Symfony\Component\Security\Core\User\User', '--empty-salt' => true, ], ['decorated' => false]); - $expected = str_replace("\n", PHP_EOL, file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt')); + $expected = str_replace("\n", \PHP_EOL, file_get_contents(__DIR__.'/app/PasswordEncode/emptysalt.txt')); $this->assertEquals($expected, $this->passwordEncoderCommandTester->getDisplay()); } @@ -51,13 +50,10 @@ public function testEncodeNoPasswordNoInteraction() 'command' => 'security:encode-password', ], ['interactive' => false]); - $this->assertContains('[ERROR] The password must not be empty.', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertEquals($statusCode, 1); + $this->assertStringContainsString('[ERROR] The password must not be empty.', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertEquals(1, $statusCode); } - /** - * @group legacy - */ public function testEncodePasswordBcrypt() { $this->setupBcrypt(); @@ -68,20 +64,17 @@ public function testEncodePasswordBcrypt() ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertContains('Password encoding succeeded', $output); + $this->assertStringContainsString('Password encoding succeeded', $output); - $encoder = new BCryptPasswordEncoder(17); + $encoder = new NativePasswordEncoder(null, null, 17, \PASSWORD_BCRYPT); preg_match('# Encoded password\s{1,}([\w+\/$.]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); } - /** - * @group legacy - */ public function testEncodePasswordArgon2i() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!($sodium = SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm not available.'); } $this->setupArgon2i(); @@ -92,9 +85,30 @@ public function testEncodePasswordArgon2i() ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertContains('Password encoding succeeded', $output); + $this->assertStringContainsString('Password encoding succeeded', $output); - $encoder = new Argon2iPasswordEncoder(); + $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, \PASSWORD_ARGON2I); + preg_match('# Encoded password\s+(\$argon2i?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); + $hash = $matches[1]; + $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); + } + + public function testEncodePasswordArgon2id() + { + if (!($sodium = (SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13'))) && !\defined('PASSWORD_ARGON2ID')) { + $this->markTestSkipped('Argon2id algorithm not available.'); + } + $this->setupArgon2id(); + $this->passwordEncoderCommandTester->execute([ + 'command' => 'security:encode-password', + 'password' => 'password', + 'user-class' => 'Custom\Class\Argon2id\User', + ], ['interactive' => false]); + + $output = $this->passwordEncoderCommandTester->getDisplay(); + $this->assertStringContainsString('Password encoding succeeded', $output); + + $encoder = $sodium ? new SodiumPasswordEncoder() : new NativePasswordEncoder(null, null, null, \PASSWORD_ARGON2ID); preg_match('# Encoded password\s+(\$argon2id?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; $this->assertTrue($encoder->isPasswordValid($hash, 'password', null)); @@ -109,7 +123,7 @@ public function testEncodePasswordNative() ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertContains('Password encoding succeeded', $output); + $this->assertStringContainsString('Password encoding succeeded', $output); $encoder = new NativePasswordEncoder(); preg_match('# Encoded password\s{1,}([\w+\/$.,=]+={0,2})\s+#', $output, $matches); @@ -130,7 +144,7 @@ public function testEncodePasswordSodium() ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertContains('Password encoding succeeded', $output); + $this->assertStringContainsString('Password encoding succeeded', $output); preg_match('# Encoded password\s+(\$?\$[\w,=\$+\/]+={0,2})\s+#', $output, $matches); $hash = $matches[1]; @@ -146,7 +160,7 @@ public function testEncodePasswordPbkdf2() ], ['interactive' => false]); $output = $this->passwordEncoderCommandTester->getDisplay(); - $this->assertContains('Password encoding succeeded', $output); + $this->assertStringContainsString('Password encoding succeeded', $output); $encoder = new Pbkdf2PasswordEncoder('sha512', true, 1000); preg_match('# Encoded password\s{1,}([\w+\/]+={0,2})\s+#', $output, $matches); @@ -165,9 +179,9 @@ public function testEncodePasswordOutput() ], ['interactive' => false] ); - $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordEmptySaltOutput() @@ -179,9 +193,9 @@ public function testEncodePasswordEmptySaltOutput() '--empty-salt' => true, ]); - $this->assertContains('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertContains(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); - $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString('Password encoding succeeded', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString(' Encoded password p@ssw0rd', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordNativeOutput() @@ -192,15 +206,12 @@ public function testEncodePasswordNativeOutput() 'user-class' => 'Custom\Class\Native\User', ], ['interactive' => false]); - $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } - /** - * @group legacy - */ public function testEncodePasswordArgon2iOutput() { - if (!Argon2iPasswordEncoder::isSupported()) { + if (!(SodiumPasswordEncoder::isSupported() && !\defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2I')) { $this->markTestSkipped('Argon2i algorithm not available.'); } @@ -211,7 +222,23 @@ public function testEncodePasswordArgon2iOutput() 'user-class' => 'Custom\Class\Argon2i\User', ], ['interactive' => false]); - $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + } + + public function testEncodePasswordArgon2idOutput() + { + if (!(SodiumPasswordEncoder::isSupported() && \defined('SODIUM_CRYPTO_PWHASH_ALG_ARGON2ID13')) && !\defined('PASSWORD_ARGON2ID')) { + $this->markTestSkipped('Argon2id algorithm not available.'); + } + + $this->setupArgon2id(); + $this->passwordEncoderCommandTester->execute([ + 'command' => 'security:encode-password', + 'password' => 'p@ssw0rd', + 'user-class' => 'Custom\Class\Argon2id\User', + ], ['interactive' => false]); + + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordSodiumOutput() @@ -227,17 +254,13 @@ public function testEncodePasswordSodiumOutput() 'user-class' => 'Custom\Class\Sodium\User', ], ['interactive' => false]); - $this->assertNotContains(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringNotContainsString(' Generated salt ', $this->passwordEncoderCommandTester->getDisplay()); } public function testEncodePasswordNoConfigForGivenUserClass() { - if (method_exists($this, 'expectException')) { - $this->expectException('\RuntimeException'); - $this->expectExceptionMessage('No encoder has been configured for account "Foo\Bar\User".'); - } else { - $this->setExpectedException('\RuntimeException', 'No encoder has been configured for account "Foo\Bar\User".'); - } + $this->expectException(\RuntimeException::class); + 10000 $this->expectExceptionMessage('No encoder has been configured for account "Foo\Bar\User".'); $this->passwordEncoderCommandTester->execute([ 'command' => 'security:encode-password', @@ -254,14 +277,14 @@ public function testEncodePasswordAsksNonProvidedUserClass() 'password' => 'password', ], ['decorated' => false]); - $this->assertContains(<<<EOTXT + $this->assertStringContainsString(<<<EOTXT For which user class would you like to encode a password? [Custom\Class\Native\User]: [0] Custom\Class\Native\User [1] Custom\Class\Pbkdf2\User [2] Custom\Class\Test\User [3] Symfony\Component\Security\Core\User\User EOTXT - , $this->passwordEncoderCommandTester->getDisplay(true)); + , $this->passwordEncoderCommandTester->getDisplay(true)); } public function testNonInteractiveEncodePasswordUsesFirstUserClass() @@ -271,17 +294,15 @@ public function testNonInteractiveEncodePasswordUsesFirstUserClass() 'password' => 'password', ], ['interactive' => false]); - $this->assertContains('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $this->passwordEncoderCommandTester->getDisplay()); + $this->assertStringContainsString('Encoder used Symfony\Component\Security\Core\Encoder\PlaintextPasswordEncoder', $this->passwordEncoderCommandTester->getDisplay()); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage There are no configured encoders for the "security" extension. - */ public function testThrowsExceptionOnNoConfiguredEncoders() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('There are no configured encoders for the "security" extension.'); $application = new ConsoleApplication(); - $application->add(new UserPasswordEncoderCommand($this->getMockBuilder(EncoderFactoryInterface::class)->getMock(), [])); + $application->add(new UserPasswordEncoderCommand($this->createMock(EncoderFactoryInterface::class), [])); $passwordEncoderCommand = $application->find('security:encode-password'); @@ -292,9 +313,11 @@ public function testThrowsExceptionOnNoConfiguredEncoders() ], ['interactive' => false]); } - protected function setUp() + protected function setUp(): void { - putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); + $kernel = $this->createKernel(['test_case' => 'PasswordEncode']); $kernel->boot(); @@ -305,14 +328,14 @@ protected function setUp() $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); } - protected function tearDown() + protected function tearDown(): void { $this->passwordEncoderCommandTester = null; + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } private function setupArgon2i() { - putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2i.yml']); $kernel->boot(); @@ -323,9 +346,20 @@ private function setupArgon2i() $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); } + private function setupArgon2id() + { + $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); + $kernel->boot(); + + $application = new Application($kernel); + + $passwordEncoderCommand = $application->get('security:encode-password'); + + $this->passwordEncoderCommandTester = new CommandTester($passwordEncoderCommand); + } + private function setupBcrypt() { - putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'bcrypt.yml']); $kernel->boot(); @@ -338,7 +372,6 @@ private function setupBcrypt() private function setupSodium() { - putenv('COLUMNS='.(119 + \strlen(PHP_EOL))); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'sodium.yml']); $kernel->boot(); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php new file mode 100644 index 0000000000000..054405274e83b --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/bundles.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\SecuredPageBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), + new SecuredPageBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml new file mode 100644 index 0000000000000..5c86da6252789 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/config.yml @@ -0,0 +1,32 @@ +imports: + - { resource: ./../config/framework.yml } + +services: + _defaults: { public: true } + + security.user.provider.array: + class: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\SecuredPageBundle\Security\Core\User\ArrayUserProvider + +security: + + encoders: + \Symfony\Component\Security\Core\User\UserInterface: plaintext + + providers: + array: + id: security.user.provider.array + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + logout: ~ + anonymous: ~ + stateless: false + + access_control: + - { path: ^/admin$, roles: ROLE_ADMIN } + - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY } + - { path: .*, roles: IS_AUTHENTICATED_FULLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/routing.yml new file mode 100644 index 0000000000000..988fa8b63ef7f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AbstractTokenCompareRoles/routing.yml @@ -0,0 +1,8 @@ +login: + path: /login + +logout: + path: /logout + +admin_bundle: + resource: '@SecuredPageBundle/Resources/config/routing.yml' diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php new file mode 100644 index 0000000000000..115dd2c357e86 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/bundles.php @@ -0,0 +1,22 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\EventBundle\EventBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), + new EventBundle(), + new TestBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/config.yml new file mode 100644 index 0000000000000..bdd94fd0afaa7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AliasedEvents/config.yml @@ -0,0 +1,2 @@ +imports: + - { resource: ./../config/framework.yml } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php new file mode 100644 index 0000000000000..d1e9eb7e0d36a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/bundles.php @@ -0,0 +1,15 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml new file mode 100644 index 0000000000000..8ee417ab3a17d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/config.yml @@ -0,0 +1,24 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + validation: { enabled: true, enable_annotations: true } + csrf_protection: true + form: true + test: ~ + default_locale: en + session: + storage_id: session.storage.mock_file + profiler: { only_exceptions: false } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator: ~ + +security: + firewalls: + secure: + pattern: ^/ + anonymous: false + stateless: true + guard: + authenticators: + - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AnonymousBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml new file mode 100644 index 0000000000000..4d11154375219 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Anonymous/routing.yml @@ -0,0 +1,5 @@ +main: + path: / + defaults: + _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + path: /app diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php index f5b85e0c059e2..c4e7e4a15bce1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AppKernel.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\SecurityBundle\Tests\Functional\app; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; @@ -46,12 +47,12 @@ public function __construct($varDir, $testCase, $rootConfig, $environment, $debu /** * {@inheritdoc} */ - public function getContainerClass() + public function getContainerClass(): string { return parent::getContainerClass().substr(md5($this->rootConfig), -16); } - public function registerBundles() + public function registerBundles(): iterable { if (!is_file($filename = $this->getProjectDir().'/'.$this->testCase.'/bundles.php')) { throw new \RuntimeException(sprintf('The bundles file "%s" does not exist.', $filename)); @@ -60,17 +61,17 @@ public function registerBundles() return include $filename; } - public function getProjectDir() + public function getProjectDir(): string { return __DIR__; } - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/'.$this->varDir.'/'.$this->testCase.'/cache/'.$this->environment; } - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/'.$this->varDir.'/'.$this->testCase.'/logs'; } @@ -91,11 +92,20 @@ public function unserialize($str) $this->__construct($a[0], $a[1], $a[2], $a[3], $a[4]); } - protected function getKernelParameters() + protected function getKernelParameters(): array { $parameters = parent::getKernelParameters(); $parameters['kernel.test_case'] = $this->testCase; return $parameters; } + + public function getContainer(): ContainerInterface + { + if (!$this->container) { + throw new \LogicException('Cannot access the container on a non-booted kernel. Did you forget to boot it?'); + } + + return parent::getContainer(); + } } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php index 535a4bf517b80..794461855cb8d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/AutowiringTypes/bundles.php @@ -13,4 +13,5 @@ new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\AutowiringBundle\AutowiringBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php new file mode 100644 index 0000000000000..9a26fb163a77d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/bundles.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml new file mode 100644 index 0000000000000..a0ed6f8e1e151 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/config.yml @@ -0,0 +1,31 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + johannes: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + remember_me: + always_remember_me: true + secret: key + anonymous: ~ + + access_control: + - { path: ^/foo, roles: ROLE_USER } + +services: + Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider: + public: true + decorates: security.user.provider.concrete.in_memory + arguments: ['@Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeUserProvider.inner'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml new file mode 100644 index 0000000000000..08975bdcb3832 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/ClearRememberMe/routing.yml @@ -0,0 +1,7 @@ +login: + path: /login + +foo: + path: /foo + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\RememberMeFooController diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php index 65a38200e759c..81f9c48b64ca8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/CsrfFormLogin/bundles.php @@ -14,4 +14,5 @@ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\CsrfFormLoginBundle\CsrfFormLoginBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php index 7928a468da7f6..b77f03be2703b 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/FirewallEntryPoint/bundles.php @@ -13,4 +13,5 @@ new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FirewallEntryPointBundle\FirewallEntryPointBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php new file mode 100644 index 0000000000000..d1e9eb7e0d36a --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/bundles.php @@ -0,0 +1,15 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +return [ + new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml new file mode 100644 index 0000000000000..7f87c307d28b5 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/config.yml @@ -0,0 +1,33 @@ +framework: + secret: test + router: { resource: "%kernel.project_dir%/%kernel.test_case%/routing.yml" } + test: ~ + default_locale: en + profiler: false + session: + storage_id: session.storage.mock_file + +services: + logger: { class: Psr\Log\NullLogger } + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator: ~ + Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController: + tags: [controller.service_arguments] + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + Jane: { password: test, roles: [ROLE_USER] } + + firewalls: + secure: + pattern: ^/ + anonymous: lazy + stateless: false + guard: + authenticators: + - Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AppCustomAuthenticator diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml new file mode 100644 index 0000000000000..146aa811a143d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/Guarded/routing.yml @@ -0,0 +1,14 @@ +main: + path: / + defaults: + _controller: Symfony\Bundle\FrameworkBundle\Controller\RedirectController::urlRedirectAction + path: /app +profile: + path: /profile + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController::profileAction + +manual_login: + path: /manual_login + defaults: + _controller: Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\GuardedBundle\AuthenticationController::manualLoginAction diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php index 7dbd6e438072f..bbb9107456b98 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/bundles.php @@ -12,6 +12,6 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\JsonLoginBundle\JsonLoginBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml index cf92920f4bc25..3522f27f13898 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/config.yml @@ -1,5 +1,8 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } + +framework: + serializer: ~ security: encoders: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml index dff93273e804b..e15e203c626cc 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLogin/custom_handlers.yml @@ -1,5 +1,5 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } security: encoders: diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php index e3aef52a2e093..bcfd17425cfd1 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/bundles.php @@ -12,5 +12,4 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), - new Symfony\Bundle\TwigBundle\TwigBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml index 622ec0f3ebfb6..80d5ec570e29d 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/JsonLoginLdap/config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } services: Symfony\Component\Ldap\Ldap: arguments: ['@Symfony\Component\Ldap\Adapter\ExtLdap\Adapter'] diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutAccess/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/LogoutWithoutSessionInvalidation/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php index ccff0d356cab9..0e34621a35ccd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/MissingUserProvider/bundles.php @@ -12,9 +12,11 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\MissingUserProviderBundle\MissingUserProviderBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), new MissingUserProviderBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml new file mode 100644 index 0000000000000..481262acb7e6c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/argon2id.yml @@ -0,0 +1,7 @@ +imports: + - { resource: config.yml } + +security: + encoders: + Custom\Class\Argon2id\User: + algorithm: argon2id diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php index bcfd17425cfd1..edf6dae14c064 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/PasswordEncode/bundles.php @@ -12,4 +12,5 @@ return [ new Symfony\Bundle\SecurityBundle\SecurityBundle(), new Symfony\Bundle\FrameworkBundle\FrameworkBundle(), + new Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php new file mode 100644 index 0000000000000..9a26fb163a77d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/bundles.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\SecurityBundle\SecurityBundle; + +return [ + new FrameworkBundle(), + new SecurityBundle(), +]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml new file mode 100644 index 0000000000000..8ffb7d8842ca7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/config.yml @@ -0,0 +1,25 @@ +imports: + - { resource: ./../config/framework.yml } + +security: + encoders: + Symfony\Component\Security\Core\User\User: plaintext + + providers: + in_memory: + memory: + users: + test: { password: test, roles: [ROLE_USER] } + + firewalls: + default: + form_login: + check_path: login + remember_me: true + require_previous_session: false + remember_me: + always_remember_me: true + secret: key + secure: auto + logout: ~ + anonymous: ~ diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/routing.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/routing.yml new file mode 100644 index 0000000000000..45e7bcc30326c --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeCookie/routing.yml @@ -0,0 +1,2 @@ +login: + path: /login diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php index 9a26fb163a77d..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/RememberMeLogout/bundles.php @@ -11,8 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php index 181618ba99e45..a52ae15f6d9bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/bundles.php @@ -11,10 +11,10 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; -use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; return [ new FrameworkBundle(), new SecurityBundle(), - new TwigBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml index d7b8ac97d9775..e49a697e52ebe 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/SecurityHelper/config.yml @@ -1,5 +1,5 @@ imports: - - { resource: ./../config/default.yml } + - { resource: ./../config/framework.yml } services: # alias the service so we can access it in the tests diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php index 95041e7ad465e..cef48bfcc4b46 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/bundles.php @@ -12,6 +12,7 @@ use Symfony\Bundle\FrameworkBundle\FrameworkBundle; use Symfony\Bundle\SecurityBundle\SecurityBundle; use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\FormLoginBundle\FormLoginBundle; +use Symfony\Bundle\SecurityBundle\Tests\Functional\Bundle\TestBundle; use Symfony\Bundle\TwigBundle\TwigBundle; return [ @@ -19,4 +20,5 @@ new SecurityBundle(), new TwigBundle(), new FormLoginBundle(), + new TestBundle(), ]; diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml index 4e2ac1e11b9d6..ad8beee94c2e0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/StandardFormLogin/config.yml @@ -27,7 +27,7 @@ security: check_path: /login_check default_target_path: /profile logout: ~ - anonymous: ~ + anonymous: lazy # This firewall is here just to check its the logout functionality second_area: @@ -38,6 +38,7 @@ security: path: /second/logout access_control: + - { path: ^/en/$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/unprotected_resource$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secure-but-not-covered-by-access-control$, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/secured-by-one-ip$, ip: 10.10.10.10, roles: IS_AUTHENTICATED_ANONYMOUSLY } diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml index 493989866a278..f578e4b510378 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/app/config/twig.yml @@ -2,3 +2,4 @@ twig: debug: '%kernel.debug%' strict_variables: '%kernel.debug%' + exception_controller: null # to be removed in 5.0 diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php index fa590a54e908d..d174e13b5cff8 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Security/FirewallMapTest.php @@ -23,14 +23,14 @@ class FirewallMapTest extends TestCase { - const ATTRIBUTE_FIREWALL_CONTEXT = '_firewall_context'; + private const ATTRIBUTE_FIREWALL_CONTEXT = '_firewall_context'; public function testGetListenersWithEmptyMap() { $request = new Request(); $map = []; - $container = $this->getMockBuilder(Container::class)->getMock(); + $container = $this->createMock(Container::class); $container->expects($this->never())->method('get'); $firewallMap = new FirewallMap($container, $map); @@ -46,7 +46,7 @@ public function testGetListenersWithInvalidParameter() $request->attributes->set(self::ATTRIBUTE_FIREWALL_CONTEXT, 'foo'); $map = []; - $container = $this->getMockBuilder(Container::class)->getMock(); + $container = $this->createMock(Container::class); $container->expects($this->never())->method('get'); $firewallMap = new FirewallMap($container, $map); @@ -60,7 +60,7 @@ public function testGetListeners() { $request = new Request(); - $firewallContext = $this->getMockBuilder(FirewallContext::class)->disableOriginalConstructor()->getMock(); + $firewallContext = $this->createMock(FirewallContext::class); $firewallConfig = new FirewallConfig('main', 'user_checker'); $firewallContext->expects($this->once())->method('getConfig')->willReturn($firewallConfig); @@ -68,19 +68,19 @@ public function testGetListeners() $listener = function () {}; $firewallContext->expects($this->once())->method('getListeners')->willReturn([$listener]); - $exceptionListener = $this->getMockBuilder(ExceptionListener::class)->disableOriginalConstructor()->getMock(); + $exceptionListener = $this->createMock(ExceptionListener::class); $firewallContext->expects($this->once())->method('getExceptionListener')->willReturn($exceptionListener); - $logoutListener = $this->getMockBuilder(LogoutListener::class)->disableOriginalConstructor()->getMock(); + $logoutListener = $this->createMock(LogoutListener::class); $firewallContext->expects($this->once())->method('getLogoutListener')->willReturn($logoutListener); - $matcher = $this->getMockBuilder(RequestMatcherInterface::class)->getMock(); + $matcher = $this->createMock(RequestMatcherInterface::class); $matcher->expects($this->once()) ->method('matches') ->with($request) ->willReturn(true); - $container = $this->getMockBuilder(Container::class)->getMock(); + $container = $this->createMock(Container::class); $container->expects($this->exactly(2))->method('get')->willReturn($firewallContext); $firewallMap = new FirewallMap($container, ['security.firewall.map.context.foo' => $matcher]); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php index d015165739324..c00b5d9e25267 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/SecurityUserValueResolverTest.php @@ -13,12 +13,12 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\SecurityBundle\SecurityUserValueResolver; +use Symfony\Bundle\SecurityBundle\Tests\Fixtures\TokenInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; use Symfony\Component\HttpKernel\Controller\ArgumentResolver\DefaultValueResolver; use Symfony\Component\HttpKernel\ControllerMetadata\ArgumentMetadata; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; -use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\User\UserInterface; /** @@ -37,8 +37,8 @@ public function testResolveNoToken() public function testResolveNoUser() { - $mock = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $mock = $this->createMock(UserInterface::class); + $token = $this->createMock(TokenInterface::class); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -59,8 +59,8 @@ public function testResolveWrongType() public function testResolve() { - $user = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $user = $this->createMock(UserInterface::class); + $token = $this->createMock(TokenInterface::class); $token->expects($this->any())->method('getUser')->willReturn($user); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -74,8 +74,8 @@ public function testResolve() public function testIntegration() { - $user = $this->getMockBuilder(UserInterface::class)->getMock(); - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $user = $this->createMock(UserInterface::class); + $token = $this->createMock(TokenInterface::class); $token->expects($this->any())->method('getUser')->willReturn($user); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -86,7 +86,7 @@ public function testIntegration() public function testIntegrationNoUser() { - $token = $this->getMockBuilder(TokenInterface::class)->getMock(); + $token = $this->createMock(TokenInterface::class); $tokenStorage = new TokenStorage(); $tokenStorage->setToken($token); @@ -94,11 +94,3 @@ public function testIntegrationNoUser() $this->assertSame([null], $argumentResolver->getArguments(Request::create('/'), function (UserInterface $user = null) {})); } } - -abstract class DummyUser implements UserInterface -{ -} - -abstract class DummySubUser extends DummyUser -{ -} diff --git a/src/Symfony/Bundle/SecurityBundle/composer.json b/src/Symfony/Bundle/SecurityBundle/composer.json index f019a09d954e4..4061646f399ff 100644 --- a/src/Symfony/Bundle/SecurityBundle/composer.json +++ b/src/Symfony/Bundle/SecurityBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/security-bundle", "type": "symfony-bundle", - "description": "Symfony SecurityBundle", + "description": "Provides a tight integration of the Security component into the Symfony full-stack framework", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,42 +16,42 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "ext-xml": "*", "symfony/config": "^4.2|^5.0", - "symfony/dependency-injection": "^4.2|^5.0", - "symfony/http-kernel": "^4.3", - "symfony/security-core": "^4.3", + "symfony/dependency-injection": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", + "symfony/polyfill-php80": "^1.16", + "symfony/security-core": "^4.4", "symfony/security-csrf": "^4.2|^5.0", "symfony/security-guard": "^4.2|^5.0", - "symfony/security-http": "^4.3" + "symfony/security-http": "^4.4.50" }, "require-dev": { + "doctrine/annotations": "^1.10.4", "symfony/asset": "^3.4|^4.0|^5.0", "symfony/browser-kit": "^4.2|^5.0", "symfony/console": "^3.4|^4.0|^5.0", "symfony/css-selector": "^3.4|^4.0|^5.0", "symfony/dom-crawler": "^3.4|^4.0|^5.0", + "symfony/expression-language": "^3.4|^4.0|^5.0", "symfony/form": "^3.4|^4.0|^5.0", - "symfony/framework-bundle": "^4.2|^5.0", - "symfony/http-foundation": "^3.4|^4.0|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", + "symfony/process": "^3.4|^4.0|^5.0", + "symfony/serializer": "^4.4|^5.0", "symfony/translation": "^3.4|^4.0|^5.0", - "symfony/twig-bundle": "^4.2|^5.0", "symfony/twig-bridge": "^3.4|^4.0|^5.0", - "symfony/process": "^3.4|^4.0|^5.0", + "symfony/twig-bundle": "^4.4|^5.0", "symfony/validator": "^3.4|^4.0|^5.0", - "symfony/var-dumper": "^3.4|^4.0|^5.0", "symfony/yaml": "^3.4|^4.0|^5.0", - "symfony/expression-language": "^3.4|^4.0|^5.0", - "doctrine/doctrine-bundle": "~1.5", - "twig/twig": "~1.34|~2.4" + "twig/twig": "^1.43|^2.13|^3.0.4" }, "conflict": { "symfony/browser-kit": "<4.2", - "symfony/twig-bundle": "<4.2", - "symfony/var-dumper": "<3.4", - "symfony/framework-bundle": "<4.2", - "symfony/console": "<3.4" + "symfony/console": "<3.4", + "symfony/framework-bundle": "<4.4", + "symfony/ldap": "<4.4", + "symfony/twig-bundle": "<4.4" }, "autoload": { "psr-4": { "Symfony\\Bundle\\SecurityBundle\\": "" }, @@ -59,10 +59,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/TwigBundle/.gitattributes b/src/Symfony/Bundle/TwigBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md index 65b7f4b9130a5..780c46466dd36 100644 --- a/src/Symfony/Bundle/TwigBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/TwigBundle/CHANGELOG.md @@ -6,6 +6,9 @@ CHANGELOG * marked the `TemplateIterator` as `internal` * added HTML comment to beginning and end of `exception_full.html.twig` + * deprecated `ExceptionController` and `PreviewErrorController` controllers, use `ErrorController` from the `HttpKernel` component instead + * deprecated all built-in error templates in favor of the new error renderer mechanism + * deprecated `twig.exception_controller` configuration option, set it to "null" and use `framework.error_controller` configuration instead 4.2.0 ----- diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php index b6b22b77a4817..9fe4b42438453 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheCacheWarmer.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\CacheWarmer; -@trigger_error('The '.TemplateCacheCacheWarmer::class.' class is deprecated since version 4.4 and will be removed in 5.0; use Twig instead.', E_USER_DEPRECATED); +@trigger_error('The '.TemplateCacheCacheWarmer::class.' class is deprecated since version 4.4 and will be removed in 5.0; use Twig instead.', \E_USER_DEPRECATED); use Psr\Container\ContainerInterface; use Symfony\Bundle\FrameworkBundle\CacheWarmer\TemplateFinderInterface; @@ -73,7 +73,7 @@ public function warmUp($cacheDir) foreach ($templates as $template) { try { - $twig->loadTemplate($template); + $twig->load($template); } catch (Error $e) { // problem during compilation, give up } @@ -102,13 +102,8 @@ public static function getSubscribedServices() /** * Find templates in the given directory. - * - * @param string $namespace The namespace for these templates - * @param string $dir The folder where to look for templates - * - * @return array An array of templates */ - private function findTemplatesInFolder($namespace, $dir) + private function findTemplatesInFolder(?string $namespace, string $dir): array { if (!is_dir($dir)) { return []; diff --git a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php index b31b344f451d2..e81ac2ab859d9 100644 --- a/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php +++ b/src/Symfony/Bundle/TwigBundle/CacheWarmer/TemplateCacheWarmer.php @@ -46,7 +46,7 @@ public function warmUp($cacheDir) foreach ($this->iterator as $template) { try { - $this->twig->loadTemplate($template); + $this->twig->load($template); } catch (Error $e) { // problem during compilation, give up // might be a syntax error or a non-Twig template diff --git a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php index c0b175c3a2ed8..cb72bc4bf78a8 100644 --- a/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php +++ b/src/Symfony/Bundle/TwigBundle/Command/LintCommand.php @@ -42,9 +42,9 @@ protected function configure() ; } - protected function findFiles($filename) + protected function findFiles($filename): iterable { - if (0 === strpos($filename, '@')) { + if (str_starts_with($filename, '@')) { $dir = $this->getApplication()->getKernel()->locateResource($filename); return Finder::create()->files()->in($dir)->name('*.twig'); diff --git a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php index e1d7760826eab..dda4ae6e82f32 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/ExceptionController.php @@ -11,13 +11,16 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Log\DebugLoggerInterface; use Twig\Environment; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; + +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), \E_USER_DEPRECATED); /** * ExceptionController renders error or exception pages for a given @@ -25,6 +28,8 @@ * * @author Fabien Potencier <fabien@symfony.com> * @author Matthias Pigulla <mp@webfactory.de> + * + * @deprecated since Symfony 4.4, use Symfony\Component\HttpKernel\Controller\ErrorController instead. */ class ExceptionController { @@ -32,8 +37,7 @@ class ExceptionController protected $debug; /** - * @param Environment $twig - * @param bool $debug Show error (false) or exception (true) pages by default + * @param bool $debug Show error (false) or exception (true) pages by default */ public function __construct(Environment $twig, bool $debug) { @@ -60,10 +64,10 @@ public function showAction(Request $request, FlattenException $exception, DebugL $code = $exception->getStatusCode(); return new Response($this->twig->render( - (string) $this->findTemplate($request, $request->getRequestFormat(), $code, $showException), + $this->findTemplate($request, $request->getRequestFormat(), $code, $showException), [ 'status_code' => $code, - 'status_text' => isset(Response::$statusTexts[$code]) ? Response::$statusTexts[$code] : '', + 'status_text' => Response::$statusTexts[$code] ?? '', 'exception' => $exception, 'logger' => $logger, 'currentContent' => $currentContent, @@ -88,10 +92,9 @@ protected function getAndCleanOutputBuffering($startObLevel) } /** - * @param Request $request - * @param string $format - * @param int $code An HTTP response status code - * @param bool $showException + * @param string $format + * @param int $code An HTTP response status code + * @param bool $showException * * @return string */ @@ -122,23 +125,28 @@ protected function findTemplate(Request $request, $format, $code, $showException return sprintf('@Twig/Exception/%s.html.twig', $showException ? 'exception_full' : $name); } - // to be removed when the minimum required version of Twig is >= 3.0 + // to be removed when the minimum required version of Twig is >= 2.0 protected function templateExists($template) { $template = (string) $template; $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface || method_exists($loader, 'exists')) { - return $loader->exists($template); - } - try { - $loader->getSourceContext($template)->getCode(); + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { + } - return true; - } catch (LoaderError $e) { + return false; } - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php index 80c79f45ee2f2..23ac0b8704d49 100644 --- a/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php +++ b/src/Symfony/Bundle/TwigBundle/Controller/PreviewErrorController.php @@ -11,16 +11,20 @@ namespace Symfony\Bundle\TwigBundle\Controller; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\HttpKernelInterface; +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use the "%s" instead.', PreviewErrorController::class, \Symfony\Component\HttpKernel\Controller\ErrorController::class), \E_USER_DEPRECATED); + /** * PreviewErrorController can be used to test error pages. * * It will create a test exception and forward it to another controller. * * @author Matthias Pigulla <mp@webfactory.de> + * + * @deprecated since Symfony 4.4, use the Symfony\Component\HttpKernel\Controller\ErrorController instead. */ class PreviewErrorController { @@ -39,7 +43,7 @@ public function previewErrorPageAction(Request $request, $code) /* * This Request mimics the parameters set by - * \Symfony\Component\HttpKernel\EventListener\ExceptionListener::duplicateRequest, with + * \Symfony\Component\HttpKernel\EventListener\ErrorListener::duplicateRequest, with * the additional "showException" flag. */ diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php index ff5a0e220796e..c21507dca8f1e 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExceptionListenerPass.php @@ -18,6 +18,8 @@ * Registers the Twig exception listener if Twig is registered as a templating engine. * * @author Fabien Potencier <fabien@symfony.com> + * + * @internal */ class ExceptionListenerPass implements CompilerPassInterface { @@ -27,14 +29,23 @@ public function process(ContainerBuilder $container) return; } - // register the exception controller only if Twig is enabled and required dependencies do exist - if (!class_exists('Symfony\Component\ErrorRenderer\Exception\FlattenException') || !interface_exists('Symfony\Component\EventDispatcher\EventSubscriberInterface')) { + // to be removed in 5.0 + // register the exception listener only if it's currently used, else use the provided by FrameworkBundle + if (null === $container->getParameter('twig.exception_listener.controller') && $container->hasDefinition('exception_listener')) { $container->removeDefinition('twig.exception_listener'); - } elseif ($container->hasParameter('templating.engines')) { + + return; + } + + if ($container->hasParameter('templating.engines')) { $engines = $container->getParameter('templating.engines'); - if (!\in_array('twig', $engines)) { - $container->removeDefinition('twig.exception_listener'); + if (\in_array('twig', $engines, true)) { + $container->removeDefinition('exception_listener'); + + return; } } + + $container->removeDefinition('twig.exception_listener'); } } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php index ba7e782378c84..de65dd5711918 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/ExtensionPass.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\TwigBundle\DependencyInjection\Compiler; +use Symfony\Bridge\Twig\Extension\AssetExtension; use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\ContainerBuilder; @@ -24,40 +25,58 @@ class ExtensionPass implements CompilerPassInterface { public function process(ContainerBuilder $container) { - if (!class_exists('Symfony\Component\Asset\Packages')) { + if (!class_exists(\Symfony\Component\Asset\Packages::class)) { $container->removeDefinition('twig.extension.assets'); } - if (!class_exists('Symfony\Component\ExpressionLanguage\Expression')) { + if (!class_exists(\Symfony\Component\ExpressionLanguage\Expression::class)) { $container->removeDefinition('twig.extension.expression'); } - if (!interface_exists('Symfony\Component\Routing\Generator\UrlGeneratorInterface')) { + if (!interface_exists(\Symfony\Component\Routing\Generator\UrlGeneratorInterface::class)) { $container->removeDefinition('twig.extension.routing'); } - if (!class_exists('Symfony\Component\Yaml\Yaml')) { + if (!class_exists(\Symfony\Component\Yaml\Yaml::class)) { $container->removeDefinition('twig.extension.yaml'); } - if ($container->has('form.extension')) { - $container->getDefinition('twig.extension.form')->addTag('twig.extension'); - $reflClass = new \ReflectionClass('Symfony\Bridge\Twig\Extension\FormExtension'); + $viewDir = \dirname((new \ReflectionClass(\Symfony\Bridge\Twig\Extension\FormExtension::class))->getFileName(), 2).'/Resources/views'; + $templateIterator = $container->getDefinition('twig.template_iterator'); + $templatePaths = $templateIterator->getArgument(2); + $cacheWarmer = null; + if ($container->hasDefinition('twig.cache_warmer')) { + $cacheWarmer = $container->getDefinition('twig.cache_warmer'); + $cacheWarmerPaths = $cacheWarmer->getArgument(2); + } + $loader = $container->getDefinition('twig.loader.native_filesystem'); - $coreThemePath = \dirname(\dirname($reflClass->getFileName())).'/Resources/views/Form'; - $container->getDefinition('twig.loader.native_filesystem')->addMethodCall('addPath', [$coreThemePath]); + if ($container->has('mailer')) { + $emailPath = $viewDir.'/Email'; + $loader->addMethodCall('addPath', [$emailPath, 'email']); + $loader->addMethodCall('addPath', [$emailPath, '!email']); + $templatePaths[$emailPath] = 'email'; + if ($cacheWarmer) { + $cacheWarmerPaths[$emailPath] = 'email'; + } + } - $paths = $container->getDefinition('twig.template_iterator')->getArgument(2); - $paths[$coreThemePath] = null; - $container->getDefinition('twig.template_iterator')->replaceArgument(2, $paths); + if ($container->has('form.extension')) { + $container->getDefinition('twig.extension.form')->addTag('twig.extension'); - if ($container->hasDefinition('twig.cache_warmer')) { - $paths = $container->getDefinition('twig.cache_warmer')->getArgument(2); - $paths[$coreThemePath] = null; - $container->getDefinition('twig.cache_warmer')->replaceArgument(2, $paths); + $coreThemePath = $viewDir.'/Form'; + $loader->addMethodCall('addPath', [$coreThemePath]); + $templatePaths[$coreThemePath] = null; + if ($cacheWarmer) { + $cacheWarmerPaths[$coreThemePath] = null; } } + $templateIterator->replaceArgument(2, $templatePaths); + if ($cacheWarmer) { + $container->getDefinition('twig.cache_warmer')->replaceArgument(2, $cacheWarmerPaths); + } + if ($container->has('router')) { $container->getDefinition('twig.extension.routing')->addTag('twig.extension'); } @@ -101,6 +120,10 @@ public function process(ContainerBuilder $container) $loader = $container->getDefinition('twig.loader.filesystem'); $loader->setMethodCalls(array_merge($twigLoader->getMethodCalls(), $loader->getMethodCalls())); + if (!method_exists(AssetExtension::class, 'getName')) { + $container->removeDefinition('templating.engine.twig'); + } + $twigLoader->clearTag('twig.loader'); } else { $container->setAlias('twig.loader.filesystem', new Alias('twig.loader.native_filesystem', false)); @@ -116,7 +139,7 @@ public function process(ContainerBuilder $container) $container->getDefinition('twig.extension.yaml')->addTag('twig.extension'); } - if (class_exists('Symfony\Component\Stopwatch\Stopwatch')) { + if (class_exists(\Symfony\Component\Stopwatch\Stopwatch::class)) { $container->getDefinition('twig.extension.debug.stopwatch')->addTag('twig.extension'); } diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php index a17d3facb676c..45413dc93253d 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigEnvironmentPass.php @@ -43,7 +43,7 @@ public function process(ContainerBuilder $container) $methodCall = ['addExtension', [$extension]]; $extensionClass = $container->getDefinition((string) $extension)->getClass(); - if (\is_string($extensionClass) && 0 === strpos($extensionClass, 'Symfony\Bridge\Twig\Extension')) { + if (\is_string($extensionClass) && str_starts_with($extensionClass, 'Symfony\Bridge\Twig\Extension')) { $twigBridgeExtensionsMethodCalls[] = $methodCall; } else { $othersExtensionsMethodCalls[] = $methodCall; diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php index 51b6d9b4f022f..9b1345d4c33f0 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Compiler/TwigLoaderPass.php @@ -33,13 +33,13 @@ public function process(ContainerBuilder $container) $found = 0; foreach ($container->findTaggedServiceIds('twig.loader', true) as $id => $attributes) { - $priority = isset($attributes[0]['priority']) ? $attributes[0]['priority'] : 0; + $priority = $attributes[0]['priority'] ?? 0; $prioritizedLoaders[$priority][] = $id; ++$found; } if (!$found) { - throw new LogicException('No twig loaders found. You need to tag at least one loader with "twig.loader"'); + throw new LogicException('No twig loaders found. You need to tag at least one loader with "twig.loader".'); } if (1 === $found) { diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php index b635a752aba8d..3ec4bc3f1e3c5 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configuration.php @@ -34,7 +34,21 @@ public function getConfigTreeBuilder() $rootNode ->children() - ->scalarNode('exception_controller')->defaultValue('twig.controller.exception::showAction')->end() + ->scalarNode('exception_controller') + ->defaultValue(static function () { + @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', \E_USER_DEPRECATED); + + return 'twig.controller.exception::showAction'; + }) + ->validate() + ->ifTrue(static function ($v) { return null !== $v; }) + ->then(static function ($v) { + @trigger_error('The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead.', \E_USER_DEPRECATED); + + return $v; + }) + ->end() + ->end() ->end() ; @@ -74,13 +88,13 @@ private function addGlobalsSection(ArrayNodeDefinition $rootNode) ->arrayNode('globals') ->normalizeKeys(false) ->useAttributeAsKey('key') - - 10000 >example(['foo' => '"@bar"', 'pi' => 3.14]) + ->example(['foo' => '@bar', 'pi' => 3.14]) ->prototype('array') ->normalizeKeys(false) ->beforeNormalization() - ->ifTrue(function ($v) { return \is_string($v) && 0 === strpos($v, '@'); }) + ->ifTrue(function ($v) { return \is_string($v) && str_starts_with($v, '@'); }) ->then(function ($v) { - if (0 === strpos($v, '@@')) { + if (str_starts_with($v, '@@')) { return substr($v, 1); } @@ -130,7 +144,7 @@ private function addTwigOptions(ArrayNodeDefinition $rootNode) ->booleanNode('debug')->defaultValue('%kernel.debug%')->end() ->booleanNode('strict_variables') ->defaultValue(function () { - @trigger_error('Relying on the default value ("false") of the "twig.strict_variables" configuration option is deprecated since Symfony 4.1. You should use "%kernel.debug%" explicitly instead, which will be the new default in 5.0.', E_USER_DEPRECATED); + @trigger_error('Relying on the default value ("false") of the "twig.strict_variables" configuration option is deprecated since Symfony 4.1. You should use "%kernel.debug%" explicitly instead, which will be the new default in 5.0.', \E_USER_DEPRECATED); return false; }) diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php index a1354622b6726..07ec691769ec7 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/Configurator/EnvironmentConfigurator.php @@ -15,7 +15,7 @@ use Twig\Environment; // BC/FC with namespaced Twig -class_exists('Twig\Environment'); +class_exists(Environment::class); /** * Twig environment configurator. diff --git a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php index 8f8b65cf2ec0b..1c0e2ff327409 100644 --- a/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php +++ b/src/Symfony/Bundle/TwigBundle/DependencyInjection/TwigExtension.php @@ -18,9 +18,11 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\Form\AbstractRendererEngine; use Symfony\Component\HttpKernel\DependencyInjection\Extension; use Symfony\Component\Mailer\Mailer; use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Service\ResetInterface; use Twig\Extension\ExtensionInterface; use Twig\Extension\RuntimeExtensionInterface; use Twig\Loader\LoaderInterface; @@ -38,11 +40,17 @@ public function load(array $configs, ContainerBuilder $container) $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); $loader->load('twig.xml'); - if (class_exists('Symfony\Component\Form\Form')) { + if (class_exists(\Symfony\Component\Form\Form::class)) { $loader->load('form.xml'); + + if (is_subclass_of(AbstractRendererEngine::class, ResetInterface::class)) { + $container->getDefinition('twig.form.engine')->addTag('kernel.reset', [ + 'method' => 'reset', + ]); + } } - if (interface_exists('Symfony\Component\Templating\EngineInterface')) { + if (interface_exists(\Symfony\Component\Templating\EngineInterface::class)) { $loader->load('templating.xml'); } @@ -122,7 +130,7 @@ public function load(array $configs, ContainerBuilder $container) if (file_exists($dir = $container->getParameter('kernel.root_dir').'/Resources/views')) { if ($dir !== $defaultTwigPath) { - @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultTwigPath), E_USER_DEPRECATED); + @trigger_error(sprintf('Loading Twig templates from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $dir, $defaultTwigPath), \E_USER_DEPRECATED); } $twigFilesystemLoaderDefinition->addMethodCall('addPath', [$dir]); @@ -145,18 +153,20 @@ public function load(array $configs, ContainerBuilder $container) } } - unset( - $config['form'], - $config['globals'], - $config['extensions'] - ); - if (isset($config['autoescape_service']) && isset($config['autoescape_service_method'])) { $config['autoescape'] = [new Reference($config['autoescape_service']), $config['autoescape_service_method']]; } - unset($config['autoescape_service'], $config['autoescape_service_method']); - $container->getDefinition('twig')->replaceArgument(1, $config); + $container->getDefinition('twig')->replaceArgument(1, array_intersect_key($config, [ + 'debug' => true, + 'charset' => true, + 'base_template_class' => true, + 'strict_variables' => true, + 'autoescape' => true, + 'cache' => true, + 'auto_reload' => true, + 'optimizations' => true, + ])); $container->registerForAutoconfiguration(\Twig_ExtensionInterface::class)->addTag('twig.extension'); $container->registerForAutoconfiguration(\Twig_LoaderInterface::class)->addTag('twig.loader'); @@ -170,14 +180,14 @@ public function load(array $configs, ContainerBuilder $container) } } - private function getBundleTemplatePaths(ContainerBuilder $container, array $config) + private function getBundleTemplatePaths(ContainerBuilder $container, array $config): array { $bundleHierarchy = []; foreach ($container->getParameter('kernel.bundles_metadata') as $name => $bundle) { $defaultOverrideBundlePath = $container->getParameterBag()->resolveValue($config['default_path']).'/bundles/'.$name; if (file_exists($dir = $container->getParameter('kernel.root_dir').'/Resources/'.$name.'/views')) { - @trigger_error(sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $name, $dir, $defaultOverrideBundlePath), E_USER_DEPRECATED); + @trigger_error(sprintf('Loading Twig templates for "%s" from the "%s" directory is deprecated since Symfony 4.2, use "%s" instead.', $name, $dir, $defaultOverrideBundlePath), \E_USER_DEPRECATED); $bundleHierarchy[$name][] = $dir; } @@ -188,7 +198,7 @@ private function getBundleTemplatePaths(ContainerBuilder $container, array $conf } $container->addResource(new FileExistenceResource($defaultOverrideBundlePath)); - if (file_exists($dir = $bundle['path'].'/Resources/views')) { + if (file_exists($dir = $bundle['path'].'/Resources/views') || file_exists($dir = $bundle['path'].'/templates')) { $bundleHierarchy[$name][] = $dir; } $container->addResource(new FileExistenceResource($dir)); @@ -197,9 +207,9 @@ private function getBundleTemplatePaths(ContainerBuilder $container, array $conf return $bundleHierarchy; } - private function normalizeBundleName($name) + private function normalizeBundleName(string $name): string { - if ('Bundle' === substr($name, -6)) { + if (str_ends_with($name, 'Bundle')) { $name = substr($name, 0, -6); } @@ -207,9 +217,7 @@ private function normalizeBundleName($name) } /** - * Returns the base path for the XSD files. - * - * @return string The XSD base path + * {@inheritdoc} */ public function getXsdValidationBasePath() { diff --git a/src/Symfony/Bundle/TwigBundle/LICENSE b/src/Symfony/Bundle/TwigBundle/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bundle/TwigBundle/LICENSE +++ b/src/Symfony/Bundle/TwigBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php index 1757a5997ced1..19fd158dc96ff 100644 --- a/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php +++ b/src/Symfony/Bundle/TwigBundle/Loader/FilesystemLoader.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\TwigBundle\Loader; -@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig notation for templates instead.', E_USER_DEPRECATED); +@trigger_error('The '.FilesystemLoader::class.' class is deprecated since version 4.3 and will be removed in 5.0; use Twig notation for templates instead.', \E_USER_DEPRECATED); use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\Templating\TemplateNameParserInterface; @@ -47,6 +47,8 @@ public function __construct(FileLocatorInterface $locator, TemplateNameParserInt * {@inheritdoc} * * The name parameter might also be a TemplateReferenceInterface. + * + * @return bool */ public function exists($name) { @@ -94,7 +96,7 @@ protected function findTemplate($template, $throw = true) throw $twigLoaderException; } - return false; + return null; } return $this->cache[$logicalName] = $file; diff --git a/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php b/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php index 9ef58d7bdbbe6..493fe250779df 100644 --- a/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php +++ b/src/Symfony/Bundle/TwigBundle/Loader/NativeFilesystemLoader.php @@ -23,6 +23,8 @@ class NativeFilesystemLoader extends FilesystemLoader { /** * {@inheritdoc} + * + * @return string|null */ protected function findTemplate($template, $throw = true) { diff --git a/src/Symfony/Bundle/TwigBundle/README.md b/src/Symfony/Bundle/TwigBundle/README.md index a9793256664cd..3ae2985baef94 100644 --- a/src/Symfony/Bundle/TwigBundle/README.md +++ b/src/Symfony/Bundle/TwigBundle/README.md @@ -1,10 +1,13 @@ TwigBundle ========== +TwigBundle provides a tight integration of Twig into the Symfony full-stack +framework. + Resources --------- - * [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) + * [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/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml index 4177da62de513..8fe29572c687c 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/form.xml @@ -6,12 +6,7 @@ <services> <defaults public="false" /> - <service id="twig.extension.form" class="Symfony\Bridge\Twig\Extension\FormExtension"> - <argument type="collection"> - <argument type="service" id="service_container" /> - <argument>twig.form.renderer</argument> - </argument> - </service> + <service id="twig.extension.form" class="Symfony\Bridge\Twig\Extension\FormExtension" /> <service id="twig.form.engine" class="Symfony\Bridge\Twig\Form\TwigRendererEngine"> <argument>%twig.form.resources%</argument> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml index 684a68873162b..709522e44dcaf 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.xml @@ -134,16 +134,19 @@ <argument>%twig.exception_listener.controller%</argument> <argument type="service" id="logger" on-invalid="null" /> <argument>%kernel.debug%</argument> + <deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated> </service> <service id="twig.controller.exception" class="Symfony\Bundle\TwigBundle\Controller\ExceptionController" public="true"> <argument type="service" id="twig" /> <argument>%kernel.debug%</argument> + <deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated> </service> <service id="twig.controller.preview_error" class="Symfony\Bundle\TwigBundle\Controller\PreviewErrorController" public="true"> <argument type="service" id="http_kernel" /> <argument>%twig.exception_listener.controller%</argument> + <deprecated>The "%service_id%" service is deprecated since Symfony 4.4.</deprecated> </service> <service id="twig.configurator.environment" class="Symfony\Bundle\TwigBundle\DependencyInjection\Configurator\EnvironmentConfigurator"> @@ -158,5 +161,17 @@ <service id="twig.runtime_loader" class="Twig\RuntimeLoader\ContainerRuntimeLoader"> <argument /> <!-- runtime locator --> </service> + + <service id="twig.error_renderer.html" class="Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer" decorates="error_renderer.html"> + <argument type="service" id="twig" /> + <argument type="service" id="twig.error_renderer.html.inner" /> + <argument type="service"> + <service class="bool"> + <factory class="Symfony\Bridge\Twig\ErrorRenderer\TwigErrorRenderer" method="isDebug" /> + <argument type="service" id="request_stack" /> + <argument>%kernel.debug%</argument> + </service> + </argument> + </service> </services> </container> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.atom.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.atom.twig index 25c84a6c9b5ec..8a9de15c2b892 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.atom.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.atom.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ include('@Twig/Exception/error.xml.twig') }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.css.twig index d8a9369487821..f2816dc8d903e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.css.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} /* {{ status_code }} {{ status_text }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.html.twig index 01fab3ad08738..75c3789510d6b 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.html.twig @@ -1,3 +1,6 @@ +{% if legacy is not defined or legacy %} + {% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} +{% endif %} <!DOCTYPE html> <html> <head> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.js.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.js.twig index d8a9369487821..f2816dc8d903e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.js.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.js.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} /* {{ status_code }} {{ status_text }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.json.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.json.twig index fc19fd83bb0e0..a675a5620d3b4 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.json.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.json.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ { 'error': { 'code': status_code, 'message': status_text } }|json_encode|raw }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.rdf.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.rdf.twig index 25c84a6c9b5ec..8a9de15c2b892 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.rdf.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.rdf.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ include('@Twig/Exception/error.xml.twig') }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.txt.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.txt.twig index bec5b1e302486..66ddee0048d49 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.txt.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.txt.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} Oops! An Error Occurred ======================= diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.xml.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.xml.twig index 5ea8f565ab9c7..5b38858eec323 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.xml.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/error.xml.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} <?xml version="1.0" encoding="{{ _charset }}" ?> <error code="{{ status_code }}" message="{{ status_text }}" /> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.atom.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.atom.twig index 2cdf03f2bcb59..ec921b96202d1 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.atom.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.atom.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ include('@Twig/Exception/exception.xml.twig', { exception: exception }) }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.css.twig index 593d490257e35..cec0e16f66d67 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.css.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} /* {{ include('@Twig/Exception/exception.txt.twig', { exception: exception }) }} */ diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.js.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.js.twig index 593d490257e35..cec0e16f66d67 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.js.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.js.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} /* {{ include('@Twig/Exception/exception.txt.twig', { exception: exception }) }} */ diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig index 13a41476f2a7b..9b87e74fd030e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.json.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ { 'error': { 'code': status_code, 'message': status_text, 'exception': exception.toarray } }|json_encode|raw }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.rdf.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.rdf.twig index 2cdf03f2bcb59..ec921b96202d1 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.rdf.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.rdf.twig @@ -1 +1,2 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {{ include('@Twig/Exception/exception.xml.twig', { exception: exception }) }} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.txt.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.txt.twig index cb17fb149f9ab..bcc15b7c32077 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.txt.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.txt.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} [exception] {{ status_code ~ ' | ' ~ status_text ~ ' | ' ~ exception.class }} [message] {{ exception.message }} {% for i, e in exception.toarray %} diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig index 36c9449b6c505..27e95641cf94e 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/exception.xml.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} <?xml version="1.0" encoding="{{ _charset }}" ?> <error code="{{ status_code }}" message="{{ status_text }}"> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig index ae46775925c53..bbab90432bd72 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/Exception/traces.xml.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} <traces> {% for trace in exception.trace %} <trace> diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig index c510a13e6632f..746ba46608ca9 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/base_js.html.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {# This file is based on WebProfilerBundle/Resources/views/Profiler/base_js.html.twig. If you make any change in this file, verify the same change is needed in the other file. #} <script{% if csp_script_nonce is defined and csp_script_nonce %} nonce="{{ csp_script_nonce }}"{% endif %}>/*<![CDATA[*/ diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig index 8c1f9af2a0d18..1dd855c0fda3a 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/exception.css.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {# This file is based on WebProfilerBundle/Resources/views/Profiler/profiler.css.twig. If you make any change in this file, verify the same change is needed in the other file. #} :root { @@ -28,6 +29,7 @@ --shadow: 0px 0px 1px rgba(128, 128, 128, .2); --border: 1px solid #e0e0e0; --background-error: var(--color-error); + --trace-selected-background: #F7E5A1; --highlight-comment: #969896; --highlight-default: #222222; --highlight-keyword: #a71d5d; diff --git a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig index faca2e7fbc795..9324a4f0c2ba6 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig +++ b/src/Symfony/Bundle/TwigBundle/Resources/views/layout.html.twig @@ -1,3 +1,4 @@ +{% deprecated 'The template "' ~ _self ~'" is deprecated since Symfony 4.4, will be removed in 5.0.' %} {% block before_html %}{% endblock %} <!DOCTYPE html> <html> @@ -21,13 +22,6 @@ <span class="hidden-xs-down">Symfony</span> Docs </a> </div> - - <div class="help-link"> - <a href="https://symfony.com/support"> - <span class="icon">{{ include('@Twig/images/icon-support.svg') }}</span> - <span class="hidden-xs-down">Symfony</span> Support - </a> - </div> </div> </header> diff --git a/src/Symfony/Bundle/TwigBundle/TemplateIterator.php b/src/Symfony/Bundle/TwigBundle/TemplateIterator.php index 3ad83ef818a32..b16eadc40bf54 100644 --- a/src/Symfony/Bundle/TwigBundle/TemplateIterator.php +++ b/src/Symfony/Bundle/TwigBundle/TemplateIterator.php @@ -30,10 +30,9 @@ class TemplateIterator implements \IteratorAggregate private $defaultPath; /** - * @param KernelInterface $kernel A KernelInterface instance - * @param string $rootDir The directory where global templates can be stored - * @param array $paths Additional Twig paths to warm - * @param string|null $defaultPath The directory where global templates can be stored + * @param string $rootDir The directory where global templates can be stored + * @param array $paths Additional Twig paths to warm + * @param string|null $defaultPath The directory where global templates can be stored */ public function __construct(KernelInterface $kernel, string $rootDir, array $paths = [], string $defaultPath = null) { @@ -44,8 +43,9 @@ public function __construct(KernelInterface $kernel, string $rootDir, array $pat } /** - * {@inheritdoc} + * @return \Traversable */ + #[\ReturnTypeWillChange] public function getIterator() { if (null !== $this->templates) { @@ -62,13 +62,15 @@ public function getIterator() } foreach ($this->kernel->getBundles() as $bundle) { $name = $bundle->getName(); - if ('Bundle' === substr($name, -6)) { + if (str_ends_with($name, 'Bundle')) { $name = substr($name, 0, -6); } + $bundleTemplatesDir = is_dir($bundle->getPath().'/Resources/views') ? $bundle->getPath().'/Resources/views' : $bundle->getPath().'/templates'; + $templates = array_merge( $templates, - $this->findTemplatesInDirectory($bundle->getPath().'/Resources/views', $name), + $this->findTemplatesInDirectory($bundleTemplatesDir, $name), $this->findTemplatesInDirectory($this->rootDir.'/Resources/'.$bundle->getName().'/views', $name) ); if (null !== $this->defaultPath) { diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php index ee7c20746ae5b..86654117962c2 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/ExceptionControllerTest.php @@ -13,11 +13,14 @@ use Symfony\Bundle\TwigBundle\Controller\ExceptionController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Twig\Environment; use Twig\Loader\ArrayLoader; +/** + * @group legacy + */ class ExceptionControllerTest extends TestCase { public function testShowActionCanBeForcedToShowErrorPage() diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php index f007e630e6147..f6d3cf8a708dd 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Controller/PreviewErrorControllerTest.php @@ -13,11 +13,14 @@ use Symfony\Bundle\TwigBundle\Controller\PreviewErrorController; use Symfony\Bundle\TwigBundle\Tests\TestCase; -use Symfony\Component\ErrorRenderer\Exception\FlattenException; +use Symfony\Component\Debug\Exception\FlattenException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\HttpKernelInterface; +/** + * @group legacy + */ class PreviewErrorControllerTest extends TestCase { public function testForwardRequestToConfiguredController() @@ -27,7 +30,7 @@ public function testForwardRequestToConfiguredController() $code = 123; $logicalControllerName = 'foo:bar:baz'; - $kernel = $this->getMockBuilder('\Symfony\Component\HttpKernel\HttpKernelInterface')->getMock(); + $kernel = $this->createMock(HttpKernelInterface::class); $kernel ->expects($this->once()) ->method('handle') diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/AcmeBundle/AcmeBundle.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/AcmeBundle/AcmeBundle.php new file mode 100644 index 0000000000000..c676380db083c --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/AcmeBundle/AcmeBundle.php @@ -0,0 +1,18 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection\AcmeBundle; + +use Symfony\Component\HttpKernel\Bundle\Bundle; + +class AcmeBundle extends Bundle +{ +} diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle1Bundle/bar.txt b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/AcmeBundle/Resources/views/layout.html.twig similarity index 100% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle1Bundle/bar.txt rename to src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/AcmeBundle/Resources/views/layout.html.twig diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExceptionListenerPassTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExceptionListenerPassTest.php new file mode 100644 index 0000000000000..2242e86a6ca98 --- /dev/null +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExceptionListenerPassTest.php @@ -0,0 +1,77 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection\Compiler; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExceptionListenerPass; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\Security\Http\Firewall\ExceptionListener; +use Twig\Environment; + +class ExceptionListenerPassTest extends TestCase +{ + public function testExitsWhenTwigIsNotAvailable() + { + $builder = new ContainerBuilder(); + $builder->register('exception_listener', ExceptionListener::class); + $builder->register('twig.exception_listener', ExceptionListener::class); + + ($pass = new ExceptionListenerPass())->process($builder); + + $this->assertTrue($builder->hasDefinition('exception_listener')); + $this->assertTrue($builder->hasDefinition('twig.exception_listener')); + } + + public function testRemovesTwigExceptionListenerWhenNoExceptionListenerControllerExists() + { + $builder = new ContainerBuilder(); + $builder->register('twig', Environment::class); + $builder->register('exception_listener', ExceptionListener::class); + $builder->register('twig.exception_listener', ExceptionListener::class); + $builder->setParameter('twig.exception_listener.controller', null); + + ($pass = new ExceptionListenerPass())->process($builder); + + $this->assertTrue($builder->hasDefinition('exception_listener')); + $this->assertFalse($builder->hasDefinition('twig.exception_listener')); + } + + public function testRemovesTwigExceptionListenerIfTwigIsNotUsedAsTemplateEngine() + { + $builder = new ContainerBuilder(); + $builder->register('twig', Environment::class); + $builder->register('exception_listener', ExceptionListener::class); + $builder->register('twig.exception_listener', ExceptionListener::class); + $builder->setParameter('twig.exception_listener.controller', 'exception_controller::showAction'); + $builder->setParameter('templating.engines', ['php']); + + ($pass = new ExceptionListenerPass())->process($builder); + + $this->assertTrue($builder->hasDefinition('exception_listener')); + $this->assertFalse($builder->hasDefinition('twig.exception_listener')); + } + + public function testRemovesKernelExceptionListenerIfTwigIsUsedAsTemplateEngine() + { + $builder = new ContainerBuilder(); + $builder->register('twig', Environment::class); + $builder->register('exception_listener', ExceptionListener::class); + $builder->register('twig.exception_listener', ExceptionListener::class); + $builder->setParameter('twig.exception_listener.controller', 'exception_controller::showAction'); + $builder->setParameter('templating.engines', ['twig']); + + ($pass = new ExceptionListenerPass())->process($builder); + + $this->assertFalse($builder->hasDefinition('exception_listener')); + $this->assertTrue($builder->hasDefinition('twig.exception_listener')); + } +} diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php index 539a952a607ab..15529a9c61f46 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/ExtensionPassTest.php @@ -12,9 +12,14 @@ namespace Symfony\Bundle\TwigBundle\Tests\DependencyInjection\Compiler; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Twig\AppVariable; use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\ExtensionPass; +use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; +use Symfony\Bundle\TwigBundle\TemplateIterator; +use Symfony\Bundle\TwigBundle\TwigEngine; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Twig\Loader\FilesystemLoader as TwigFilesystemLoader; class ExtensionPassTest extends TestCase { @@ -23,21 +28,24 @@ public function testProcessDoesNotDropExistingFileLoaderMethodCalls() $container = new ContainerBuilder(); $container->setParameter('kernel.debug', false); - $container->register('twig.app_variable', '\Symfony\Bridge\Twig\AppVariable'); - $container->register('templating', '\Symfony\Bundle\TwigBundle\TwigEngine'); + $container->register('twig.app_variable', AppVariable::class); + $container->register('templating', TwigEngine::class); $container->register('twig.extension.yaml'); $container->register('twig.extension.debug.stopwatch'); $container->register('twig.extension.expression'); - $nativeTwigLoader = new Definition('\Twig\Loader\FilesystemLoader'); + $nativeTwigLoader = new Definition(TwigFilesystemLoader::class); $nativeTwigLoader->addMethodCall('addPath', []); $container->setDefinition('twig.loader.native_filesystem', $nativeTwigLoader); - $filesystemLoader = new Definition('\Symfony\Bundle\TwigBundle\Loader\FilesystemLoader'); + $filesystemLoader = new Definition(FilesystemLoader::class); $filesystemLoader->setArguments([null, null, null]); $filesystemLoader->addMethodCall('addPath', []); $container->setDefinition('twig.loader.filesystem', $filesystemLoader); + $templateIterator = new Definition(TemplateIterator::class, [null, null, null]); + $container->setDefinition('twig.template_iterator', $templateIterator); + $extensionPass = new ExtensionPass(); $extensionPass->process($container); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php index 3b65273d6d731..68431f969fe10 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Compiler/TwigLoaderPassTest.php @@ -15,6 +15,7 @@ use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\TwigLoaderPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; +use Symfony\Component\DependencyInjection\Exception\LogicException; class TwigLoaderPassTest extends TestCase { @@ -31,7 +32,7 @@ class TwigLoaderPassTest extends TestCase */ private $pass; - protected function setUp() + protected function setUp(): void { $this->builder = new ContainerBuilder(); $this->builder->register('twig'); @@ -87,11 +88,9 @@ public function testMapperPassWithTwoTaggedLoadersWithPriority() $this->assertEquals('test_loader_1', (string) $calls[1][1][0]); } - /** - * @expectedException \Symfony\Component\DependencyInjection\Exception\LogicException - */ public function testMapperPassWithZeroTaggedLoaders() { + $this->expectException(LogicException::class); $this->pass->process($this->builder); } } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php index e479804b987e1..522db25a6cfe3 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -21,6 +21,7 @@ public function testDoNoDuplicateDefaultFormResources() { $input = [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 'form_themes' => ['form_div_layout.html.twig'], ]; @@ -42,10 +43,23 @@ public function testGetStrictVariablesDefaultFalse() $this->assertFalse($config['strict_variables']); } + /** + * @group legacy + * @expectedDeprecation The "twig.exception_controller" configuration key has been deprecated in Symfony 4.4, set it to "null" and use "framework.error_controller" configuration key instead. + */ + public function testGetExceptionControllerDefault() + { + $processor = new Processor(); + $config = $processor->processConfiguration(new Configuration(), [['exception_controller' => 'exception_controller']]); + + $this->assertSame('exception_controller', $config['exception_controller']); + } + public function testGlobalsAreNotNormalized() { $input = [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 relying on default 'globals' => ['some-global' => true], ]; @@ -59,6 +73,7 @@ public function testArrayKeysInGlobalsAreNotNormalized() { $input = [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 relying on default 'globals' => ['global' => ['some-key' => 'some-value']], ]; diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php index de55467d2285e..ab5cf941c0311 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/customTemplateEscapingGuesser.php @@ -4,4 +4,5 @@ 'autoescape_service' => 'my_project.some_bundle.template_escaping_guesser', 'autoescape_service_method' => 'guess', 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php index 76e66160f50d6..fcc1402151ecb 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/empty.php @@ -2,4 +2,5 @@ $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php index 69fbf7c012e88..c4383a671a626 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/formats.php @@ -12,4 +12,5 @@ 'thousands_separator' => '.', ], 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php index 8dd2be40960b1..18d0ba50f90f2 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/php/full.php @@ -17,6 +17,7 @@ 'charset' => 'ISO-8859-1', 'debug' => true, 'strict_variables' => true, + 'exception_controller' => null, // to be removed in 5.0 'default_path' => '%kernel.project_dir%/Fixtures/templates', 'paths' => [ 'path1', diff --git a/src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle2Bundle/foo.txt b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/templates/bundles/AcmeBundle/layout.html.twig similarity index 100% rename from src/Symfony/Component/HttpKernel/Tests/Fixtures/Bundle2Bundle/foo.txt rename to src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/templates/bundles/AcmeBundle/layout.html.twig diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/templates/bundles/TwigBundle/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/templates/bundles/TwigBundle/layout.html.twig deleted file mode 100644 index bb07ecfe55a36..0000000000000 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/templates/bundles/TwigBundle/layout.html.twig +++ /dev/null @@ -1 +0,0 @@ -This is a layout diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/customTemplateEscapingGuesser.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/customTemplateEscapingGuesser.xml index 63c851720cc28..5d2558467ba6b 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/customTemplateEscapingGuesser.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/customTemplateEscapingGuesser.xml @@ -6,5 +6,5 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - <twig:config autoescape-service="my_project.some_bundle.template_escaping_guesser" autoescape-service-method="guess" strict-variables="false" /> + <twig:config autoescape-service="my_project.some_bundle.template_escaping_guesser" autoescape-service-method="guess" strict-variables="false" exception-controller="null" /> </container> diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/empty.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/empty.xml index ffe2f5257733c..0affe9386c31c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/empty.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/empty.xml @@ -6,5 +6,5 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - <twig:config strict-variables="false" /> + <twig:config strict-variables="false" exception-controller="null" /> </container> diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml index 17bcf0acd4490..f95f052104f37 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/extra.xml @@ -6,7 +6,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - <twig:config auto-reload="true" autoescape="true" base-template-class="stdClass" cache="/tmp" charset="ISO-8859-1" debug="true" strict-variables="true"> + <twig:config auto-reload="true" autoescape="true" base-template-class="stdClass" cache="/tmp" charset="ISO-8859-1" debug="true" strict-variables="true" exception-controller="null"> <twig:path namespace="namespace3">namespaced_path3</twig:path> </twig:config> </container> diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/formats.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/formats.xml index 7f8fb84357da0..c14a971998f84 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/formats.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/formats.xml @@ -5,7 +5,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - <twig:config strict-variables="false"> + <twig:config strict-variables="false" exception-controller="null"> <twig:date format="Y-m-d" interval-format="%d" timezone="Europe/Berlin" /> <twig:number-format decimals="2" decimal-point="," thousands-separator="." /> </twig:config> diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml index 8ece3b80b794d..665230134766e 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/xml/full.xml @@ -6,7 +6,7 @@ xsi:schemaLocation="http://symfony.com/schema/dic/services https://symfony.com/schema/dic/services/services-1.0.xsd http://symfony.com/schema/dic/twig https://symfony.com/schema/dic/twig/twig-1.0.xsd"> - <twig:config auto-reload="true" autoescape="true" base-template-class="stdClass" cache="/tmp" charset="ISO-8859-1" debug="true" strict-variables="true" default-path="%kernel.project_dir%/Fixtures/templates"> + <twig:config auto-reload="true" autoescape="true" base-template-class="stdClass" cache="/tmp" charset="ISO-8859-1" debug="true" strict-variables="true" default-path="%kernel.project_dir%/Fixtures/templates" exception-controller="null"> <twig:form-theme>MyBundle::form.html.twig</twig:form-theme> <twig:global key="foo" id="bar" type="service" /> <twig:global key="baz">@@qux</twig:global> diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/customTemplateEscapingGuesser.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/customTemplateEscapingGuesser.yml index 34e301c0957e5..de2c45759c9fd 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/customTemplateEscapingGuesser.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/customTemplateEscapingGuesser.yml @@ -2,3 +2,4 @@ twig: autoescape_service: my_project.some_bundle.template_escaping_guesser autoescape_service_method: guess strict_variables: false # to be removed in 5.0 relying on default + exception_controller: ~ # to be removed in 5.0 relying on default diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/empty.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/empty.yml index 9b5dbcf35b67b..e66cce095371a 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/empty.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/empty.yml @@ -1,2 +1,3 @@ twig: strict_variables: false # to be removed in 5.0 relying on default + exception_controller: ~ # to be removed in 5.0 relying on default diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/extra.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/extra.yml index 41a281cc8198c..7a7cbae6b1010 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/extra.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/extra.yml @@ -1,4 +1,5 @@ twig: strict_variables: false # to be removed in 5.0 relying on default + exception_controller: ~ # to be removed in 5.0 relying on default paths: namespaced_path3: namespace3 diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/formats.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/formats.yml index a5c57f383edfe..b54e50aea1804 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/formats.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/formats.yml @@ -1,5 +1,6 @@ twig: strict_variables: false # to be removed in 5.0 relying on default + exception_controller: ~ # to be removed in 5.0 relying on default date: format: Y-m-d interval_format: '%d' diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml index 24c10c23fe8f1..0c28f048781d9 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/Fixtures/yml/full.yml @@ -14,6 +14,7 @@ twig: debug: true strict_variables: true default_path: '%kernel.project_dir%/Fixtures/templates' + exception_controller: ~ # to be removed in 5.0 relying on default paths: path1: '' path2: '' diff --git a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php index 1057540dccaac..84f08949c7248 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/DependencyInjection/TwigExtensionTest.php @@ -13,6 +13,7 @@ use Symfony\Bundle\TwigBundle\DependencyInjection\Compiler\RuntimeLoaderPass; use Symfony\Bundle\TwigBundle\DependencyInjection\TwigExtension; +use Symfony\Bundle\TwigBundle\Tests\DependencyInjection\AcmeBundle\AcmeBundle; use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Component\Config\FileLocator; use Symfony\Component\DependencyInjection\Compiler\PassConfig; @@ -22,6 +23,7 @@ use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\ParameterBag\ParameterBag; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; class TwigExtensionTest extends TestCase { @@ -31,6 +33,7 @@ public function testLoadEmptyConfiguration() $container->registerExtension(new TwigExtension()); $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 ]); $this->compileContainer($container); @@ -156,6 +159,7 @@ public function testGlobalsWithDifferentTypesAndValues() $container->loadFromExtension('twig', [ 'globals' => $globals, 'strict_variables' => false, // // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 relying on default ]); $this->compileContainer($container); @@ -182,7 +186,7 @@ public function testTwigLoaderPaths($format) $def = $container->getDefinition('twig.loader.native_filesystem'); $paths = []; foreach ($def->getMethodCalls() as $call) { - if ('addPath' === $call[0] && false === strpos($call[1][0], 'Form')) { + if ('addPath' === $call[0] && !str_contains($call[1][0], 'Form')) { $paths[] = $call[1]; } } @@ -193,9 +197,9 @@ public function testTwigLoaderPaths($format) ['namespaced_path1', 'namespace1'], ['namespaced_path2', 'namespace2'], ['namespaced_path3', 'namespace3'], - [__DIR__.'/Fixtures/templates/bundles/TwigBundle', 'Twig'], - [realpath(__DIR__.'/../..').'/Resources/views', 'Twig'], - [realpath(__DIR__.'/../..').'/Resources/views', '!Twig'], + [__DIR__.'/Fixtures/templates/bundles/AcmeBundle', 'Acme'], + [__DIR__.'/AcmeBundle/Resources/views', 'Acme'], + [__DIR__.'/AcmeBundle/Resources/views', '!Acme'], [__DIR__.'/Fixtures/templates'], ], $paths); } @@ -203,7 +207,7 @@ public function testTwigLoaderPaths($format) /** * @group legacy * @dataProvider getFormats - * @expectedDeprecation Loading Twig templates for "TwigBundle" from the "%s/Resources/TwigBundle/views" directory is deprecated since Symfony 4.2, use "%s/templates/bundles/TwigBundle" instead. + * @expectedDeprecation Loading Twig templates for "AcmeBundle" from the "%s/Resources/AcmeBundle/views" directory is deprecated since Symfony 4.2, use "%s/templates/bundles/AcmeBundle" instead. * @expectedDeprecation Loading Twig templates from the "%s/Resources/views" directory is deprecated since Symfony 4.2, use "%s/templates" instead. */ public function testLegacyTwigLoaderPaths($format) @@ -217,7 +221,7 @@ public function testLegacyTwigLoaderPaths($format) $def = $container->getDefinition('twig.loader.native_filesystem'); $paths = []; foreach ($def->getMethodCalls() as $call) { - if ('addPath' === $call[0] && false === strpos($call[1][0], 'Form')) { + if ('addPath' === $call[0] && !str_contains($call[1][0], 'Form')) { $paths[] = $call[1]; } } @@ -228,10 +232,10 @@ public function testLegacyTwigLoaderPaths($format) ['namespaced_path1', 'namespace1'], ['namespaced_path2', 'namespace2'], ['namespaced_path3', 'namespace3'], - [__DIR__.'/../Fixtures/templates/Resources/TwigBundle/views', 'Twig'], - [__DIR__.'/Fixtures/templates/bundles/TwigBundle', 'Twig'], - [realpath(__DIR__.'/../..').'/Resources/views', 'Twig'], - [realpath(__DIR__.'/../..').'/Resources/views', '!Twig'], + [__DIR__.'/../Fixtures/templates/Resources/AcmeBundle/views', 'Acme'], + [__DIR__.'/Fixtures/templates/bundles/AcmeBundle', 'Acme'], + [__DIR__.'/AcmeBundle/Resources/views', 'Acme'], + [__DIR__.'/AcmeBundle/Resources/views', '!Acme'], [__DIR__.'/../Fixtures/templates/Resources/views'], [__DIR__.'/Fixtures/templates'], ], $paths); @@ -259,6 +263,7 @@ public function testStopwatchExtensionAvailability($debug, $stopwatchEnabled, $e $container->registerExtension(new TwigExtension()); $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 relying on default ]); $container->setAlias('test.twig.extension.debug.stopwatch', 'twig.extension.debug.stopwatch')->setPublic(true); $this->compileContainer($container); @@ -289,6 +294,7 @@ public function testRuntimeLoader() $container->registerExtension(new TwigExtension()); $container->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 relying on default ]); $container->setParameter('kernel.environment', 'test'); $container->setParameter('debug.file_link_format', 'test'); @@ -297,8 +303,10 @@ public function testRuntimeLoader() $container->register('templating.locator', 'FooClass'); $container->register('templating.name_parser', 'FooClass'); $container->register('foo', '%foo%')->addTag('twig.runtime'); + $container->register('error_renderer.html', HtmlErrorRenderer::class); $container->addCompilerPass(new RuntimeLoaderPass(), PassConfig::TYPE_BEFORE_REMOVING); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); $loader = $container->getDefinition('twig.runtime_loader'); @@ -318,12 +326,12 @@ private function createContainer(string $rootDir = __DIR__.'/Fixtures') 'kernel.charset' => 'UTF-8', 'kernel.debug' => false, 'kernel.bundles' => [ - 'TwigBundle' => 'Symfony\\Bundle\\TwigBundle\\TwigBundle', + 'AcmeBundle' => AcmeBundle::class, ], 'kernel.bundles_metadata' => [ - 'TwigBundle' => [ - 'namespace' => 'Symfony\\Bundle\\TwigBundle', - 'path' => realpath(__DIR__.'/../..'), + 'AcmeBundle' => [ + 'namespace' => 'Symfony\Bundle\TwigBundle\Tests\DependencyInjection\AcmeBundle', + 'path' => __DIR__.'/AcmeBundle', ], ], ])); @@ -335,6 +343,7 @@ private function compileContainer(ContainerBuilder $container) { $container->getCompilerPassConfig()->setOptimizationPasses([]); $container->getCompilerPassConfig()->setRemovingPasses([]); + $container->getCompilerPassConfig()->setAfterRemovingPasses([]); $container->compile(); } @@ -353,7 +362,7 @@ private function loadFromFile(ContainerBuilder $container, $file, $format) $loader = new YamlFileLoader($container, $locator); break; default: - throw new \InvalidArgumentException(sprintf('Unsupported format: %s', $format)); + throw new \InvalidArgumentException(sprintf('Unsupported format: "%s"', $format)); } $loader->load($file.'.'.$format); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Fixtures/templates/Resources/TwigBundle/views/layout.html.twig b/src/Symfony/Bundle/TwigBundle/Tests/Fixtures/templates/Resources/AcmeBundle/views/layout.html.twig similarity index 100% rename from src/Symfony/Bundle/TwigBundle/Tests/Fixtures/templates/Resources/TwigBundle/views/layout.html.twig rename to src/Symfony/Bundle/TwigBundle/Tests/Fixtures/templates/Resources/AcmeBundle/views/layout.html.twig diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php index ca21df09029b9..b397d3abace14 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/CacheWarmingTest.php @@ -47,12 +47,12 @@ public function testCacheIsProperlyWarmedWhenTemplatingIsDisabled() $this->assertFileExists($kernel->getCacheDir().'/twig'); } - protected function setUp() + protected function setUp(): void { $this->deleteTempDir(); } - protected function tearDown() + protected function tearDown(): void { $this->deleteTempDir(); } @@ -79,12 +79,12 @@ public function __construct($withTemplating) parent::__construct(($withTemplating ? 'with' : 'without').'_templating', true); } - public function getName() + public function getName(): string { return 'CacheWarming'; } - public function registerBundles() + public function registerBundles(): iterable { return [new FrameworkBundle(), new TwigBundle()]; } @@ -99,6 +99,7 @@ public function registerContainerConfiguration(LoaderInterface $loader) ]) ->loadFromExtension('twig', [ // to be removed in 5.0 relying on default 'strict_variables' => false, + 'exception_controller' => null, ]) ; }); @@ -115,17 +116,17 @@ public function registerContainerConfiguration(LoaderInterface $loader) } } - public function getProjectDir() + public function getProjectDir(): string { return __DIR__; } - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/CacheWarmingKernel/cache/'.$this->environment; } - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/CacheWarmingKernel/logs'; } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/EmptyAppTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/EmptyAppTest.php index df90b237526e6..a7f3bc27dadec 100644 --- a/src/Symfony/Bundle/TwigBundle/ 17AE Tests/Functional/EmptyAppTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/EmptyAppTest.php @@ -14,6 +14,9 @@ use Symfony\Bundle\TwigBundle\Tests\TestCase; use Symfony\Bundle\TwigBundle\TwigBundle; use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\HttpKernel\Kernel; class EmptyAppTest extends TestCase @@ -26,32 +29,54 @@ public function testBootEmptyApp() $this->assertTrue($kernel->getContainer()->hasParameter('twig.default_path')); $this->assertNotEmpty($kernel->getContainer()->getParameter('twig.default_path')); } + + protected function setUp(): void + { + $this->deleteTempDir(); + } + + protected function tearDown(): void + { + $this->deleteTempDir(); + } + + private function deleteTempDir() + { + if (!file_exists($dir = sys_get_temp_dir().'/'.Kernel::VERSION.'/EmptyAppKernel')) { + return; + } + + $fs = new Filesystem(); + $fs->remove($dir); + } } class EmptyAppKernel extends Kernel { - public function registerBundles() + public function registerBundles(): iterable { return [new TwigBundle()]; } public function registerContainerConfiguration(LoaderInterface $loader) { - $loader->load(function ($container) { - $container - ->loadFromExtension('twig', [ // to be removed in 5.0 relying on default - 'strict_variables' => false, - ]) - ; + $loader->load(static function (ContainerBuilder $container) { + $container->loadFromExtension('twig', [ // to be removed in 5.0 relying on default + 'strict_variables' => false, + 'exception_controller' => null, + ]); + $container->register('error_renderer.html', HtmlErrorRenderer::class); + $container->setAlias('error_renderer', 'error_renderer.html'); + $container->setParameter('debug.file_link_format', null); }); } - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/EmptyAppKernel/cache/'.$this->environment; } - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/EmptyAppKernel/logs'; } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php index f1e77090721b9..0961ffe22c58c 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Functional/NoTemplatingEntryTest.php @@ -27,15 +27,15 @@ public function test() $container = $kernel->getContainer(); $content = $container->get('twig')->render('index.html.twig'); - $this->assertContains('{ a: b }', $content); + $this->assertStringContainsString('{ a: b }', $content); } - protected function setUp() + protected function setUp(): void { $this->deleteTempDir(); } - protected function tearDown() + protected function tearDown(): void { $this->deleteTempDir(); } @@ -53,7 +53,7 @@ protected function deleteTempDir() class NoTemplatingEntryKernel extends Kernel { - public function registerBundles() + public function registerBundles(): iterable { return [new FrameworkBundle(), new TwigBundle()]; } @@ -68,18 +68,19 @@ public function registerContainerConfiguration(LoaderInterface $loader) ]) ->loadFromExtension('twig', [ 'strict_variables' => false, // to be removed in 5.0 relying on default + 'exception_controller' => null, // to be removed in 5.0 'default_path' => __DIR__.'/templates', ]) ; }); } - public function getCacheDir() + public function getCacheDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/NoTemplatingEntryKernel/cache/'.$this->environment; } - public function getLogDir() + public function getLogDir(): string { return sys_get_temp_dir().'/'.Kernel::VERSION.'/NoTemplatingEntryKernel/logs'; } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php index b19a1180fcc4c..0f43aa847bdc7 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Loader/FilesystemLoaderTest.php @@ -13,7 +13,10 @@ use Symfony\Bundle\TwigBundle\Loader\FilesystemLoader; use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Symfony\Component\Config\FileLocatorInterface; +use Symfony\Component\Templating\TemplateNameParserInterface; use Symfony\Component\Templating\TemplateReferenceInterface; +use Twig\Error\LoaderError; /** * @group legacy @@ -22,8 +25,8 @@ class FilesystemLoaderTest extends TestCase { public function testGetSourceContext() { - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $parser = $this->createMock(TemplateNameParserInterface::class); + $locator = $this->createMock(FileLocatorInterface::class); $locator ->expects($this->once()) ->method('locate') @@ -42,8 +45,8 @@ public function testGetSourceContext() public function testExists() { // should return true for templates that Twig does not find, but Symfony does - $parser = $this->getMockBuilde F41A r('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $parser = $this->createMock(TemplateNameParserInterface::class); + $locator = $this->createMock(FileLocatorInterface::class); $locator ->expects($this->once()) ->method('locate') @@ -54,20 +57,18 @@ public function testExists() $this->assertTrue($loader->exists($template)); } - /** - * @expectedException \Twig\Error\LoaderError - */ public function testTwigErrorIfLocatorThrowsInvalid() { - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); + $this->expectException(LoaderError::class); + $parser = $this->createMock(TemplateNameParserInterface::class); $parser ->expects($this->once()) ->method('parse') ->with('name.format.engine') - ->willReturn($this->getMockBuilder(TemplateReferenceInterface::class)->getMock()) + ->willReturn($this->createMock(TemplateReferenceInterface::class)) ; - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $locator = $this->createMock(FileLocatorInterface::class); $locator ->expects($this->once()) ->method('locate') @@ -78,20 +79,18 @@ public function testTwigErrorIfLocatorThrowsInvalid() $loader->getCacheKey('name.format.engine'); } - /** - * @expectedException \Twig\Error\LoaderError - */ public function testTwigErrorIfLocatorReturnsFalse() { - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); + $this->expectException(LoaderError::class); + $parser = $this->createMock(TemplateNameParserInterface::class); $parser ->expects($this->once()) ->method('parse') ->with('name.format.engine') - ->willReturn($this->getMockBuilder(TemplateReferenceInterface::class)->getMock()) + ->willReturn($this->createMock(TemplateReferenceInterface::class)) ; - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $locator = $this->createMock(FileLocatorInterface::class); $locator ->expects($this->once()) ->method('locate') @@ -102,33 +101,31 @@ public function testTwigErrorIfLocatorReturnsFalse() $loader->getCacheKey('name.format.engine'); } - /** - * @expectedException \Twig\Error\LoaderError - * @expectedExceptionMessageRegExp /Unable to find template "name\.format\.engine" \(looked into: .*Tests.Loader.\.\..DependencyInjection.Fixtures.templates\)/ - */ public function testTwigErrorIfTemplateDoesNotExist() { - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $this->expectException(LoaderError::class); + $this->expectExceptionMessageMatches('/Unable to find template "name\.format\.engine" \(looked into: .*Tests.Loader.\.\..DependencyInjection.Fixtures.templates\)/'); + $parser = $this->createMock(TemplateNameParserInterface::class); + $locator = $this->createMock(FileLocatorInterface::class); $loader = new FilesystemLoader($locator, $parser); $loader->addPath(__DIR__.'/../DependencyInjection/Fixtures/templates'); - $method = new \ReflectionMethod('Symfony\Bundle\TwigBundle\Loader\FilesystemLoader', 'findTemplate'); + $method = new \ReflectionMethod(FilesystemLoader::class, 'findTemplate'); $method->setAccessible(true); $method->invoke($loader, 'name.format.engine'); } public function testTwigSoftErrorIfTemplateDoesNotExist() { - $parser = $this->getMockBuilder('Symfony\Component\Templating\TemplateNameParserInterface')->getMock(); - $locator = $this->getMockBuilder('Symfony\Component\Config\FileLocatorInterface')->getMock(); + $parser = $this->createMock(TemplateNameParserInterface::class); + $locator = $this->createMock(FileLocatorInterface::class); $loader = new FilesystemLoader($locator, $parser); $loader->addPath(__DIR__.'/../DependencyInjection/Fixtures/templates'); - $method = new \ReflectionMethod('Symfony\Bundle\TwigBundle\Loader\FilesystemLoader', 'findTemplate'); + $method = new \ReflectionMethod(FilesystemLoader::class, 'findTemplate'); $method->setAccessible(true); - $this->assertFalse($method->invoke($loader, 'name.format.engine', false)); + $this->assertNull($method->invoke($loader, 'name.format.engine', false)); } } diff --git a/src/Symfony/Bundle/TwigBundle/Tests/Loader/NativeFilesystemLoaderTest.php b/src/Symfony/Bundle/TwigBundle/Tests/Loader/NativeFilesystemLoaderTest.php index b017a766ddd86..e4b1ec456fa8a 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/Loader/NativeFilesystemLoaderTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/Loader/NativeFilesystemLoaderTest.php @@ -1,9 +1,19 @@ <?php +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Bundle\TwigBundle\Tests\Loader; use Symfony\Bundle\TwigBundle\Loader\NativeFilesystemLoader; use Symfony\Bundle\TwigBundle\Tests\TestCase; +use Twig\Error\LoaderError; class NativeFilesystemLoaderTest extends TestCase { @@ -15,24 +25,20 @@ public function testWithNativeNamespace() $this->assertSame('Fixtures'.\DIRECTORY_SEPARATOR.'templates'.\DIRECTORY_SEPARATOR.'Foo'.\DIRECTORY_SEPARATOR.'index.html.twig', $loader->getCacheKey('@Test/Foo/index.html.twig')); } - /** - * @expectedException \Twig\Error\LoaderError - * @expectedExceptionMessage Template reference "TestBundle::Foo/index.html.twig" not found, did you mean "@Test/Foo/index.html.twig"? - */ public function testWithLegacyStyle1() { + $this->expectException(LoaderError::class); + $this->expectExceptionMessage('Template reference "TestBundle::Foo/index.html.twig" not found, did you mean "@Test/Foo/index.html.twig"?'); $loader = new NativeFilesystemLoader(null, __DIR__.'/../'); $loader->addPath('Fixtures/templates', 'Test'); $loader->getCacheKey('TestBundle::Foo/index.html.twig'); } - /** - * @expectedException \Twig\Error\LoaderError - * @expectedExceptionMessage Template reference "TestBundle:Foo:index.html.twig" not found, did you mean "@Test/Foo/index.html.twig"? - */ public function testWithLegacyStyle2() { + $this->expectException(LoaderError::class); + $this->expectExceptionMessage('Template reference "TestBundle:Foo:index.html.twig" not found, did you mean "@Test/Foo/index.html.twig"?'); $loader = new NativeFilesystemLoader(null, __DIR__.'/../'); $loader->addPath('Fixtures/templates', 'Test'); diff --git a/src/Symfony/Bundle/TwigBundle/Tests/TemplateIteratorTest.php b/src/Symfony/Bundle/TwigBundle/Tests/TemplateIteratorTest.php index b9092af3ebd0c..3c0b9c5b7e38a 100644 --- a/src/Symfony/Bundle/TwigBundle/Tests/TemplateIteratorTest.php +++ b/src/Symfony/Bundle/TwigBundle/Tests/TemplateIteratorTest.php @@ -12,16 +12,18 @@ namespace Symfony\Bundle\TwigBundle\Tests; use Symfony\Bundle\TwigBundle\TemplateIterator; +use Symfony\Component\HttpKernel\Bundle\BundleInterface; +use Symfony\Component\HttpKernel\Kernel; class TemplateIteratorTest extends TestCase { public function testGetIterator() { - $bundle = $this->getMockBuilder('Symfony\Component\HttpKernel\Bundle\BundleInterface')->getMock(); + $bundle = $this->createMock(BundleInterface::class); $bundle->expects($this->any())->method('getName')->willReturn('BarBundle'); $bundle->expects($this->any())->method('getPath')->willReturn(__DIR__.'/Fixtures/templates/BarBundle'); - $kernel = $this->getMockBuilder('Symfony\Component\HttpKernel\Kernel')->disableOriginalConstructor()->getMock(); + $kernel = $this->createMock(Kernel::class); $kernel->expects($this->any())->method('getBundles')->willReturn([ $bundle, ]); diff --git a/src/Symfony/Bundle/TwigBundle/TwigBundle.php b/src/Symfony/Bundle/TwigBundle/TwigBundle.php index bd766c15219e7..8b214bcf8e346 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigBundle.php +++ b/src/Symfony/Bundle/TwigBundle/TwigBundle.php @@ -20,6 +20,20 @@ use Symfony\Component\DependencyInjection\Compiler\PassConfig; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; +use Twig\Cache\FilesystemCache; +use Twig\Extension\CoreExtension; +use Twig\Extension\EscaperExtension; +use Twig\Extension\OptimizerExtension; +use Twig\Extension\StagingExtension; +use Twig\ExtensionSet; + +// Help opcache.preload discover always-needed symbols +class_exists(FilesystemCache::class); +class_exists(CoreExtension::class); +class_exists(EscaperExtension::class); +class_exists(OptimizerExtension::class); +class_exists(StagingExtension::class); +class_exists(ExtensionSet::class); /** * Bundle. @@ -32,7 +46,8 @@ public function build(ContainerBuilder $container) { parent::build($container); - $container->addCompilerPass(new ExtensionPass()); + // ExtensionPass must be run before the FragmentRendererPass as it adds tags that are processed later + $container->addCompilerPass(new ExtensionPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10); $container->addCompilerPass(new TwigEnvironmentPass()); $container->addCompilerPass(new TwigLoaderPass()); $container->addCompilerPass(new ExceptionListenerPass()); diff --git a/src/Symfony/Bundle/TwigBundle/TwigEngine.php b/src/Symfony/Bundle/TwigBundle/TwigEngine.php index b8a4995fcd4f0..78594b1e15e1b 100644 --- a/src/Symfony/Bundle/TwigBundle/TwigEngine.php +++ b/src/Symfony/Bundle/TwigBundle/TwigEngine.php @@ -11,11 +11,10 @@ namespace Symfony\Bundle\TwigBundle; -@trigger_error('The '.TwigEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use \Twig\Environment instead.', E_USER_DEPRECATED); +@trigger_error('The '.TwigEngine::class.' class is deprecated since version 4.3 and will be removed in 5.0; use \Twig\Environment instead.', \E_USER_DEPRECATED); use Symfony\Bridge\Twig\TwigEngine as BaseEngine; use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface; -use Symfony\Bundle\FrameworkBundle\Templating\TemplateReference; use Symfony\Component\Config\FileLocatorInterface; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Templating\TemplateNameParserInterface; @@ -40,28 +39,6 @@ public function __construct(Environment $environment, TemplateNameParserInterfac $this->locator = $locator; } - /** - * {@inheritdoc} - */ - public function render($name, array $parameters = []) - { - try { - return parent::render($name, $parameters); - } catch (Error $e) { - if ($name instanceof TemplateReference && !method_exists($e, 'setSourceContext')) { - try { - // try to get the real name of the template where the error occurred - $name = $e->getTemplateName(); - $path = (string) $this->locator->locate($this->parser->parse($name)); - $e->setTemplateName($path); - } catch (\Exception $e2) { - } - } - - throw $e; - } - } - /** * {@inheritdoc} * diff --git a/src/Symfony/Bundle/TwigBundle/composer.json b/src/Symfony/Bundle/TwigBundle/composer.json index fd6c08debe11b..4170021e233bb 100644 --- a/src/Symfony/Bundle/TwigBundle/composer.json +++ b/src/Symfony/Bundle/TwigBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/twig-bundle", "type": "symfony-bundle", - "description": "Symfony TwigBundle", + "description": "Provides a tight integration of Twig into the Symfony full-stack framework", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,13 +16,13 @@ } ], "require": { - "php": "^7.1.3", - "symfony/error-renderer": "^4.4|^5.0", + "php": ">=7.1.3", "symfony/twig-bridge": "^4.4|^5.0", "symfony/http-foundation": "^4.3|^5.0", - "symfony/http-kernel": "^4.4|^5.0", + "symfony/http-kernel": "^4.4", "symfony/polyfill-ctype": "~1.8", - "twig/twig": "~1.41|~2.10" + "symfony/polyfill-php80": "^1.16", + "twig/twig": "^1.43|^2.13|^3.0.4" }, "require-dev": { "symfony/asset": "^3.4|^4.0|^5.0", @@ -37,12 +37,12 @@ "symfony/yaml": "^3.4|^4.0|^5.0", "symfony/framework-bundle": "^4.4|^5.0", "symfony/web-link": "^3.4|^4.0|^5.0", - "doctrine/annotations": "~1.0", - "doctrine/cache": "~1.0" + "doctrine/annotations": "^1.10.4", + "doctrine/cache": "^1.0|^2.0" }, "conflict": { "symfony/dependency-injection": "<4.1", - "symfony/framework-bundle": "<4.3", + "symfony/framework-bundle": "<4.4", "symfony/translation": "<4.2" }, "autoload": { @@ -51,10 +51,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/WebProfilerBundle/.gitattributes b/src/Symfony/Bundle/WebProfilerBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md index 821a86b75668b..257924f0aad55 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebProfilerBundle/CHANGELOG.md @@ -4,9 +4,14 @@ CHANGELOG 4.4.0 ----- - * Added button to clear the ajax request tab - * Deprecated the `ExceptionController::templateExists()` method - * Deprecated the `TemplateManager::templateExists()` method + * added support for the Mailer component + * added support for the HttpClient component + * added button to clear the ajax request tab + * deprecated the `ExceptionController::templateExists()` method + * deprecated the `TemplateManager::templateExists()` method + * deprecated the `ExceptionController` in favor of `ExceptionPanelController` + * marked all classes of the WebProfilerBundle as internal + * added a section with the stamps of a message after it is dispatched in the Messenger panel 4.3.0 ----- diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php index f9298965bf1e0..b5d7b3ff2cfd1 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionController.php @@ -11,7 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; -use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -19,11 +19,16 @@ use Twig\Environment; use Twig\Error\LoaderError; use Twig\Loader\ExistsLoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; + +@trigger_error(sprintf('The "%s" class is deprecated since Symfony 4.4, use "%s" instead.', ExceptionController::class, ExceptionPanelController::class), \E_USER_DEPRECATED); /** * ExceptionController. * * @author Fabien Potencier <fabien@symfony.com> + * + * @deprecated since Symfony 4.4, use the ExceptionPanelController instead. */ class ExceptionController { @@ -37,11 +42,7 @@ public function __construct(Profiler $profiler = null, Environment $twig, bool $ $this->profiler = $profiler; $this->twig = $twig; $this->debug = $debug; - $this->errorRenderer = $errorRenderer; - - if (null === $errorRenderer) { - $this->errorRenderer = new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat); - } + $this->errorRenderer = $errorRenderer ?? new HtmlErrorRenderer($debug, $this->twig->getCharset(), $fileLinkFormat); } /** @@ -101,7 +102,7 @@ public function cssAction($token) $template = $this->getTemplate(); - if (!$this->templateExists($template, false)) { + if (!$this->templateExists($template)) { return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']); } @@ -113,27 +114,25 @@ protected function getTemplate() return '@Twig/Exception/'.($this->debug ? 'exception' : 'error').'.html.twig'; } - /** - * @deprecated since Symfony 4.4 - */ - protected function templateExists($template/*, bool $triggerDeprecation = true */) + protected function templateExists($template) { - if (1 === \func_num_args()) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use the "exists()" method of the Twig loader instead.', __METHOD__), E_USER_DEPRECATED); - } - $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface) { - return $loader->exists($template); - } - try { - $loader->getSource($template); + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { + } - return true; - } catch (LoaderError $e) { + return false; } - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php new file mode 100644 index 0000000000000..4941208c88bc2 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ExceptionPanelController.php @@ -0,0 +1,61 @@ +<?php + +/* + * This file is part of the Symfony package. + * + * (c) Fabien Potencier <fabien@symfony.com> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\WebProfilerBundle\Controller; + +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +use Symfony\Component\HttpKernel\Profiler\Profiler; + +/** + * Renders the exception panel. + * + * @author Yonel Ceruto <yonelceruto@gmail.com> + * + * @internal + */ +class ExceptionPanelController +{ + private $errorRenderer; + private $profiler; + + public function __construct(HtmlErrorRenderer $errorRenderer, Profiler $profiler = null) + { + $this->errorRenderer = $errorRenderer; + $this->profiler = $profiler; + } + + /** + * Renders the exception panel stacktrace for the given token. + */ + public function body(string $token): Response + { + if (null === $this->profiler) { + throw new NotFoundHttpException('The profiler must be enabled.'); + } + + $exception = $this->profiler->loadProfile($token) + ->getCollector('exception') + ->getException() + ; + + return new Response($this->errorRenderer->getBody($exception), 200, ['Content-Type' => 'text/html']); + } + + /** + * Renders the exception panel stylesheet. + */ + public function stylesheet(): Response + { + return new Response($this->errorRenderer->getStylesheet(), 200, ['Content-Type' => 'text/css']); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php index 410f20287198f..5872a9e07c1ac 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/ProfilerController.php @@ -17,6 +17,8 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Profiler\Profiler; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; @@ -24,6 +26,8 @@ /** * @author Fabien Potencier <fabien@symfony.com> + * + * @internal since Symfony 4.4 */ class ProfilerController { @@ -62,8 +66,7 @@ public function homeAction() /** * Renders a profiler panel for the given token. * - * @param Request $request The current HTTP request - * @param string $token The profiler token + * @param string $token The profiler token * * @return Response A Response instance * @@ -77,7 +80,7 @@ public function panelAction(Request $request, $token) $this->cspHandler->disableCsp(); } - $panel = $request->query->get('panel', 'request'); + $panel = $request->query->get('panel'); $page = $request->query->get('page', 'home'); if ('latest' === $token && $latest = current($this->profiler->find(null, null, 1, null, null, null))) { @@ -85,14 +88,30 @@ public function panelAction(Request $request, $token) } if (!$profile = $this->profiler->loadProfile($token)) { - return new Response($this->twig->render('@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request]), 200, ['Content-Type' => 'text/html']); + return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/info.html.twig', ['about' => 'no_token', 'token' => $token, 'request' => $request]); + } + + if (null === $panel) { + $panel = 'request'; + + foreach ($profile->getCollectors() as $collector) { + if ($collector instanceof ExceptionDataCollector && $collector->hasException()) { + $panel = $collector->getName(); + + break; + } + + if ($collector instanceof DumpDataCollector && $collector->getDumpsCount() > 0) { + $panel = $collector->getName(); + } + } } if (!$profile->hasCollector($panel)) { throw new NotFoundHttpException(sprintf('Panel "%s" is not available for token "%s".', $panel, $token)); } - return new Response($this->twig->render($this->getTemplateManager()->getName($profile, $panel), [ + return $this->renderWithCspNonces($request, $this->getTemplateManager()->getName($profile, $panel), [ 'token' => $token, 'profile' => $profile, 'collector' => $profile->getCollector($panel), @@ -102,14 +121,13 @@ public function panelAction(Request $request, $token) 'templates' => $this->getTemplateManager()->getNames($profile), 'is_ajax' => $request->isXmlHttpRequest(), 'profiler_markup_version' => 2, // 1 = original profiler, 2 = Symfony 2.8+ profiler - ]), 200, ['Content-Type' => 'text/html']); + ]); } /** * Renders the Web Debug Toolbar. * - * @param Request $request The current HTTP Request - * @param string $token The profiler token + * @param string $token The profiler token * * @return Response A Response instance * @@ -121,7 +139,7 @@ public function toolbarAction(Request $request, $token) throw new NotFoundHttpException('The profiler must be enabled.'); } - if ($request->hasSession() && ($session = $request->getSession()) && $session->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { + if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { // keep current flashes for one more request if using AutoExpireFlashBag $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); } @@ -210,8 +228,7 @@ public function searchBarAction(Request $request) /** * Renders the search results. * - * @param Request $request The current HTTP Request - * @param string $token The token + * @param string $token The token * * @return Response A Response instance * @@ -235,7 +252,7 @@ public function searchResultsAction(Request $request, $token) $end = $request->query->get('end', null); $limit = $request->query->get('limit'); - return new Response($this->twig->render('@WebProfiler/Profiler/results.html.twig', [ + return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/results.html.twig', [ 'request' => $request, 'token' => $token, 'profile' => $profile, @@ -248,7 +265,7 @@ public function searchResultsAction(Request $request, $token) 'end' => $end, 'limit' => $limit, 'panel' => null, - ]), 200, ['Content-Type' => 'text/html']); + ]); } /** @@ -350,11 +367,11 @@ public function openAction(Request $request) throw new NotFoundHttpException(sprintf('The file "%s" cannot be opened.', $file)); } - return new Response($this->twig->render('@WebProfiler/Profiler/open.html.twig', [ + return $this->renderWithCspNonces($request, '@WebProfiler/Profiler/open.html.twig', [ 'filename' => $filename, 'file' => $file, 'line' => $line, - ]), 200, ['Content-Type' => 'text/html']); + ]); } /** @@ -380,14 +397,14 @@ private function denyAccessIfProfilerDisabled() $this->profiler->disable(); } - private function renderWithCspNonces(Request $request, $template, $variables, $code = 200, $headers = ['Content-Type' => 'text/html']) + private function renderWithCspNonces(Request $request, string $template, array $variables, int $code = 200, array $headers = ['Content-Type' => 'text/html']): Response { $response = new Response('', $code, $headers); $nonces = $this->cspHandler ? $this->cspHandler->getNonces($request, $response) : []; - $variables['csp_script_nonce'] = isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null; - $variables['csp_style_nonce'] = isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null; + $variables['csp_script_nonce'] = $nonces['csp_script_nonce'] ?? null; + $variables['csp_style_nonce'] = $nonces['csp_style_nonce'] ?? null; $response->setContent($this->twig->render($template, $variables)); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php index f3f68fe5d83b5..d18a5b6c1d61c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Controller/RouterController.php @@ -11,6 +11,7 @@ namespace Symfony\Bundle\WebProfilerBundle\Controller; +use Symfony\Component\ExpressionLanguage\ExpressionFunctionProviderInterface; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; @@ -26,6 +27,8 @@ * RouterController. * * @author Fabien Potencier <fabien@symfony.com> + * + * @internal since Symfony 4.4 */ class RouterController { @@ -34,12 +37,18 @@ class RouterController private $matcher; private $routes; - public function __construct(Profiler $profiler = null, Environment $twig, UrlMatcherInterface $matcher = null, RouteCollection $routes = null) + /** + * @var ExpressionFunctionProviderInterface[] + */ + private $expressionLanguageProviders = []; + + public function __construct(Profiler $profiler = null, Environment $twig, UrlMatcherInterface $matcher = null, RouteCollection $routes = null, iterable $expressionLanguageProviders = []) { $this->profiler = $profiler; $this->twig = $twig; $this->matcher = $matcher; $this->routes = (null === $routes && $matcher instanceof RouterInterface) ? $matcher->getRouteCollection() : $routes; + $this->expressionLanguageProviders = $expressionLanguageProviders; } /** @@ -83,7 +92,7 @@ private function getTraces(RequestDataCollector $request, string $method): array $traceRequest = Request::create( $request->getPathInfo(), $request->getRequestServer(true)->get('REQUEST_METHOD'), - [], + \in_array($request->getMethod(), ['DELETE', 'PATCH', 'POST', 'PUT'], true) ? $request->getRequestRequest()->all() : $request->getRequestQuery()->all(), $request->getRequestCookies(true)->all(), [], $request->getRequestServer(true)->all() @@ -92,6 +101,9 @@ private function getTraces(RequestDataCollector $request, string $method): array $context = $this->matcher->getContext(); $context->setMethod($method); $matcher = new TraceableUrlMatcher($this->routes, $context); + foreach ($this->expressionLanguageProviders as $provider) { + $matcher->addExpressionLanguageProvider($provider); + } return $matcher->getTracesForRequest($traceRequest); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php index a38e7c686fd0a..f8fce16690b80 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Csp/ContentSecurityPolicyHandler.php @@ -38,10 +38,8 @@ public function __construct(NonceGenerator $nonceGenerator) * - The request - In case HTML content is fetched via AJAX and inserted in DOM, it must use the same nonce as origin * - The response - A call to getNonces() has already been done previously. Same nonce are returned * - They are otherwise randomly generated - * - * @return array */ - public function getNonces(Request $request, Response $response) + public function getNonces(Request $request, Response $response): array { if ($request->headers->has('X-SymfonyProfiler-Script-Nonce') && $request->headers->has('X-SymfonyProfiler-Style-Nonce')) { return [ @@ -83,7 +81,7 @@ public function disableCsp() * * @return array Nonces used by the bundle in Content-Security-Policy header */ - public function updateResponseHeaders(Request $request, Response $response) + public function updateResponseHeaders(Request $request, Response $response): array { if ($this->cspDisabled) { $this->removeCspHeaders($response); @@ -113,10 +111,8 @@ private function removeCspHeaders(Response $response) /** * Updates Content-Security-Policy headers in a response. - * - * @return array */ - private function updateCspHeaders(Response $response, array $nonces = []) + private function updateCspHeaders(Response $response, array $nonces = []): array { $nonces = array_replace([ 'csp_script_nonce' => $this->generateNonce(), @@ -127,18 +123,30 @@ private function updateCspHeaders(Response $response, array $nonces = []) $headers = $this->getCspHeaders($response); + $types = [ + 'script-src' => 'csp_script_nonce', + 'script-src-elem' => 'csp_script_nonce', + 'style-src' => 'csp_style_nonce', + 'style-src-elem' => 'csp_style_nonce', + ]; + foreach ($headers as $header => $directives) { - foreach (['script-src' => 'csp_script_nonce', 'style-src' => 'csp_style_nonce'] as $type => $tokenName) { + foreach ($types as $type => $tokenName) { if ($this->authorizesInline($directives, $type)) { continue; } if (!isset($headers[$header][$type])) { - if (isset($headers[$header]['default-src'])) { - $headers[$header][$type] = $headers[$header]['default-src']; - } else { - // If there is no script-src/style-src and no default-src, no additional rules required. + if (null === $fallback = $this->getDirectiveFallback($directives, $type)) { continue; } + + if (['\'none\''] === $fallback) { + // Fallback came from "default-src: 'none'" + // 'none' is invalid if it's not the only expression in the source list, so we leave it out + $fallback = []; + } + + $headers[$header][$type] = $fallback; } $ruleIsSet = true; if (!\in_array('\'unsafe-inline\'', $headers[$header][$type], true)) { @@ -161,22 +169,16 @@ private function updateCspHeaders(Response $response, array $nonces = []) /** * Generates a valid Content-Security-Policy nonce. - * - * @return string */ - private function generateNonce() + private function generateNonce(): string { return $this->nonceGenerator->generate(); } /** * Converts a directive set array into Content-Security-Policy header. - * - * @param array $directives The directive set - * - * @return string The Content-Security-Policy header */ - private function generateCspHeader(array $directives) + private function generateCspHeader(array $directives): string { return array_reduce(array_keys($directives), function ($res, $name) use ($directives) { return ('' !== $res ? $res.'; ' : '').sprintf('%s %s', $name, implode(' ', $directives[$name])); @@ -185,12 +187,8 @@ private function generateCspHeader(array $directives) /** * Converts a Content-Security-Policy header value into a directive set array. - * - * @param string $header The header value - * - * @return array The directive set */ - private function parseDirectives($header) + private function parseDirectives(string $header): array { $directives = []; @@ -208,29 +206,22 @@ private function parseDirectives($header) /** * Detects if the 'unsafe-inline' is prevented for a directive within the directive set. - * - * @param array $directivesSet The directive set - * @param string $type The name of the directive to check - * - * @return bool */ - private function authorizesInline(array $directivesSet, $type) + private function authorizesInline(array $directivesSet, string $type): bool { if (isset($directivesSet[$type])) { $directives = $directivesSet[$type]; - } elseif (isset($directivesSet['default-src'])) { - $directives = $directivesSet['default-src']; - } else { + } elseif (null === $directives = $this->getDirectiveFallback($directivesSet, $type)) { return false; } return \in_array('\'unsafe-inline\'', $directives, true) && !$this->hasHashOrNonce($directives); } - private function hasHashOrNonce(array $directives) + private function hasHashOrNonce(array $directives): bool { foreach ($directives as $directive) { - if ('\'' !== substr($directive, -1)) { + if (!str_ends_with($directive, '\'')) { continue; } if ('\'nonce-' === substr($directive, 0, 7)) { @@ -244,13 +235,21 @@ private function hasHashOrNonce(array $directives) return false; } + private function getDirectiveFallback(array $directiveSet, $type) + { + if (\in_array($type, ['script-src-elem', 'style-src-elem'], true) || !isset($directiveSet['default-src'])) { + // Let the browser fallback on it's own + return null; + } + + return $directiveSet['default-src']; + } + /** * Retrieves the Content-Security-Policy headers (either X-Content-Security-Policy or Content-Security-Policy) from * a response. - * - * @return array An associative array of headers */ - private function getCspHeaders(Response $response) + private function getCspHeaders(Response $response): array { $headers = []; diff --git a/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php b/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php index 728043551f3ee..19af8496908de 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Csp/NonceGenerator.php @@ -20,7 +20,7 @@ */ class NonceGenerator { - public function generate() + public function generate(): string { return bin2hex(random_bytes(16)); } diff --git a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php index 594e7fa3a7b47..05aa911354df5 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/DependencyInjection/WebProfilerExtension.php @@ -37,8 +37,7 @@ class WebProfilerExtension extends Extension /** * Loads the web profiler configuration. * - * @param array $configs An array of configuration settings - * @param ContainerBuilder $container A ContainerBuilder instance + * @param array $configs An array of configuration settings */ public function load(array $configs, ContainerBuilder $container) { @@ -62,9 +61,7 @@ public function load(array $configs, ContainerBuilder $container) } /** - * Returns the base path for the XSD files. - * - * @return string The XSD base path + * {@inheritdoc} */ public function getXsdValidationBasePath() { diff --git a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php index 1541c7113b138..cb47ff7398553 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php +++ b/src/Symfony/Bundle/WebProfilerBundle/EventListener/WebDebugToolbarListener.php @@ -35,8 +35,8 @@ */ class WebDebugToolbarListener implements EventSubscriberInterface { - const DISABLED = 1; - const ENABLED = 2; + public const DISABLED = 1; + public const ENABLED = 2; protected $twig; protected $urlGenerator; @@ -88,8 +88,7 @@ public function onKernelResponse(FilterResponseEvent $event) } if ($response->headers->has('X-Debug-Token') && $response->isRedirect() && $this->interceptRedirects && 'html' === $request->getRequestFormat()) { - $session = $request->getSession(); - if (null !== $session && $session->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { + if ($request->hasSession() && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag) { // keep current flashes for one more request if using AutoExpireFlashBag $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); } @@ -102,9 +101,9 @@ public function onKernelResponse(FilterResponseEvent $event) if (self::DISABLED === $this->mode || !$response->headers->has('X-Debug-Token') || $response->isRedirection() - || ($response->headers->has('Content-Type') && false === strpos($response->headers->get('Content-Type'), 'html')) + || ($response->headers->has('Content-Type') && !str_contains($response->headers->get('Content-Type'), 'html')) || 'html' !== $request->getRequestFormat() - || false !== stripos($response->headers->get('Content-Disposition'), 'attachment;') + || false !== stripos($response->headers->get('Content-Disposition', ''), 'attachment;') ) { return; } @@ -127,8 +126,8 @@ protected function injectToolbar(Response $response, Request $request, array $no 'excluded_ajax_paths' => $this->excludedAjaxPaths, 'token' => $response->headers->get('X-Debug-Token'), 'request' => $request, - 'csp_script_nonce' => isset($nonces['csp_script_nonce']) ? $nonces['csp_script_nonce'] : null, - 'csp_style_nonce' => isset($nonces['csp_style_nonce']) ? $nonces['csp_style_nonce'] : null, + 'csp_script_nonce' => $nonces['csp_script_nonce'] ?? null, + 'csp_style_nonce' => $nonces['csp_style_nonce'] ?? null, ] ))."\n"; $content = substr($content, 0, $pos).$toolbar.substr($content, $pos); diff --git a/src/Symfony/Bundle/WebProfilerBundle/LICENSE b/src/Symfony/Bundle/WebProfilerBundle/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/LICENSE +++ b/src/Symfony/Bundle/WebProfilerBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php index 77cf4073d3da9..f7fa05ab4f98d 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Profiler/TemplateManager.php @@ -24,6 +24,8 @@ * * @author Fabien Potencier <fabien@symfony.com> * @author Artur WielogΓ³rski <wodor@wodor.net> + * + * @internal since Symfony 4.4 */ class TemplateManager { @@ -41,8 +43,7 @@ public function __construct(Profiler $profiler, Environment $twig, array $templa /** * Gets the template name for a given panel. * - * @param Profile $profile - * @param string $panel + * @param string $panel * * @return mixed * @@ -75,13 +76,13 @@ public function getNames(Profile $profile) continue; } - list($name, $template) = $arguments; + [$name, $template] = $arguments; if (!$this->profiler->has($name) || !$profile->hasCollector($name)) { continue; } - if ('.html.twig' === substr($template, -10)) { + if (str_ends_with($template, '.html.twig')) { $template = substr($template, 0, -10); } @@ -98,28 +99,29 @@ public function getNames(Profile $profile) /** * @deprecated since Symfony 4.4 */ - protected function templateExists($template/*, bool $triggerDeprecation = true */) + protected function templateExists($template/* , bool $triggerDeprecation = true */) { if (1 === \func_num_args()) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use the "exists()" method of the Twig loader instead.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.4, use the "exists()" method of the Twig loader instead.', __METHOD__), \E_USER_DEPRECATED); } $loader = $this->twig->getLoader(); - if ($loader instanceof ExistsLoaderInterface) { - return $loader->exists($template); - } - try { - if ($loader instanceof SourceContextLoaderInterface || method_exists($loader, 'getSourceContext')) { - $loader->getSourceContext($template); - } else { - $loader->getSource($template); + if (1 === Environment::MAJOR_VERSION && !$loader instanceof ExistsLoaderInterface) { + try { + if ($loader instanceof SourceContextLoaderInterface) { + $loader->getSourceContext($template); + } else { + $loader->getSource($template); + } + + return true; + } catch (LoaderError $e) { } - return true; - } catch (LoaderError $e) { + return false; } - return false; + return $loader->exists($template); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/README.md b/src/Symfony/Bundle/WebProfilerBundle/README.md index 48e6075636519..e3c1400b1c6e9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/README.md +++ b/src/Symfony/Bundle/WebProfilerBundle/README.md @@ -1,7 +1,7 @@ WebProfilerBundle ================= -The Web profiler bundle is a **development tool** that gives detailed +WebProfilerBundle provides a **development tool** that gives detailed information about the execution of any request. **Never** enable it on production servers as it will lead to major security @@ -10,7 +10,7 @@ vulnerabilities in your project. Resources --------- - * [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) + * [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/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml index dcacc51032f31..c1c3c8a65e07f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/profiler.xml @@ -20,6 +20,8 @@ <argument type="service" id="profiler" on-invalid="null" /> <argument type="service" id="twig" /> <argument type="service" id="router" on-invalid="null" /> + <argument>null</argument> + <argument type="tagged_iterator" tag="routing.expression_language_provider" /> </service> <service id="web_profiler.controller.exception" class="Symfony\Bundle\WebProfilerBundle\Controller\ExceptionController" public="true"> @@ -27,7 +29,13 @@ <argument type="service" id="twig" /> <argument>%kernel.debug%</argument> <argument type="service" id="debug.file_link_formatter" /> - <argument type="service" id="error_renderer.renderer.html" on-invalid="null" /> + <argument type="service" id="error_handler.error_renderer.html" /> + <deprecated>The "%service_id%" service is deprecated since Symfony 4.4, use the "web_profiler.controller.exception_panel" service instead.</deprecated> + </service> + + <service id="web_profiler.controller.exception_panel" class="Symfony\Bundle\WebProfilerBundle\Controller\ExceptionPanelController" public="true"> + <argument type="service" id="error_handler.error_renderer.html" /> + <argument type="service" id="profiler" on-invalid="null" /> </service> <service id="web_profiler.csp.handler" class="Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler"> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml index 0bc9a9ec4f7ca..f20cba0e673f9 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/config/routing/profiler.xml @@ -37,11 +37,11 @@ </route> <route id="_profiler_exception" path="/{token}/exception"> - <default key="_controller">web_profiler.controller.exception::showAction</default> + <default key="_controller">web_profiler.controller.exception_panel::body</default> </route> <route id="_profiler_exception_css" path="/{token}/exception.css"> - <default key="_controller">web_profiler.controller.exception::cssAction</default> + <default key="_controller">web_profiler.controller.exception_panel::stylesheet</default> </route> </routes> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig index cbc705f51cb0e..0c406e9442c1e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/cache.html.twig @@ -108,9 +108,9 @@ <div class="metric"> <span class="value"> {% if key == 'time' %} - {{ '%0.2f'|format(1000 * value.value) }} <span class="unit">ms</span> + {{ '%0.2f'|format(1000 * value) }} <span class="unit">ms</span> {% elseif key == 'hit_read_ratio' %} - {{ value.value ?? 0 }} <span class="unit">%</span> + {{ value ?? 0 }} <span class="unit">%</span> {% else %} {{ value }} {% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig index bfed460ab7c99..6dfd27bcbc67a 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/config.html.twig @@ -101,7 +101,7 @@ {% endblock %} {% block menu %} - <span class="label label-status-{{ collector.symfonyState == 'eol' ? 'red' : collector.symfonyState in ['eom', 'dev'] ? 'yellow' : '' }}"> + <span class="label label-status-{{ collector.symfonyState == 'eol' ? 'red' : collector.symfonyState in ['eom', 'dev'] ? 'yellow' }}"> <span class="icon">{{ include('@WebProfiler/Icon/config.svg') }}</span> <strong>Configuration</strong> </span> @@ -116,14 +116,14 @@ <span class="label">Symfony version</span> </div> - {% if 'n/a' != collector.env %} + {% if 'n/a' is not same as(collector.env) %} <div class="metric"> <span class="value">{{ collector.env }}</span> <span class="label">Environment</span> </div> {% endif %} - {% if 'n/a' != collector.debug %} + {% if 'n/a' is not same as(collector.debug) %} <div class="metric"> <span class="value">{{ collector.debug ? 'enabled' : 'disabled' }}</span> <span class="label">Debug</span> @@ -153,7 +153,7 @@ <td class="font-normal">{{ collector.symfonyeom }}</td> <td class="font-normal">{{ collector.symfonyeol }}</td> <td class="font-normal"> - <a href="https://symfony.com/roadmap?version={{ collector.symfonyminorversion }}#checker">View roadmap</a> + <a href="https://symfony.com/releases/{{ collector.symfonyminorversion }}#release-checker">View roadmap</a> </td> </tr> </tbody> @@ -210,7 +210,7 @@ <thead> <tr> <th class="key">Name</th> - <th>Path</th> + <th>Class</th> </tr> </thead> <tbody> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig index 78752853b92da..aad7625a22489 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.css.twig @@ -1,7 +1,5 @@ -{{ include('@Twig/exception.css.twig') }} - .container { - max-width: auto; + max-width: none; margin: 0; padding: 0; } @@ -30,5 +28,5 @@ } .exception-message-wrapper .container { - min-height: auto; + min-height: unset; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig index 94dfbb6acac0a..261d5cc2b1871 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/exception.html.twig @@ -4,6 +4,7 @@ {% if collector.hasexception %} <style> {{ render(path('_profiler_exception_css', { token: token })) }} + {{ include('@WebProfiler/Collector/exception.css.twig') }} </style> {% endif %} {{ parent() }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig index 4ca49e7c5ff90..d99ad4f77946b 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/form.html.twig @@ -4,7 +4,7 @@ {% block toolbar %} {% if collector.data.nb_errors > 0 or collector.data.forms|length %} - {% set status_color = collector.data.nb_errors ? 'red' : '' %} + {% set status_color = collector.data.nb_errors ? 'red' %} {% set icon %} {{ include('@WebProfiler/Icon/form.svg') }} <span class="sf-toolbar-value"> @@ -90,7 +90,8 @@ cursor: pointer; padding: 5px 7px 5px 22px; position: relative; - + overflow: hidden; + text-overflow: ellipsis; } .tree .toggle-button { /* provide a bigger clickable area than just 10x10px */ @@ -449,7 +450,7 @@ {% import _self as tree %} {% set has_error = data.errors is defined and data.errors|length > 0 %} <li> - <div class="tree-inner" data-tab-target-id="{{ data.id }}-details"> + <div class="tree-inner" data-tab-target-id="{{ data.id }}-details" title="{{ name|default('(no name)') }}"> {% if has_error %} <div class="badge-error">{{ data.errors|length }}</div> {% endif %} @@ -663,7 +664,7 @@ </table> {% else %} <div class="empty"> - <p>No options where passed when constructing this form.</p> + <p>No options were passed when constructing this form.</p> </div> {% endif %} </div> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig new file mode 100644 index 0000000000000..68716153dafd5 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/http_client.html.twig @@ -0,0 +1,98 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% if collector.requestCount %} + {% set icon %} + {{ include('@WebProfiler/Icon/http-client.svg') }} + {% set status_color = '' %} + <span class="sf-toolbar-value">{{ collector.requestCount }}</span> + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { link: profiler_url, status: status_color }) }} + {% endif %} +{% endblock %} + +{% block menu %} +<span class="label {{ collector.requestCount == 0 ? 'disabled' }}"> + <span class="icon">{{ include('@WebProfiler/Icon/http-client.svg') }}</span> + <strong>HTTP Client</strong> + {% if collector.requestCount %} + <span class="count"> + {{ collector.requestCount }} + </span> + {% endif %} +</span> +{% endblock %} + +{% block panel %} + <h2>HTTP Client</h2> + {% if collector.requestCount == 0 %} + <div class="empty"> + <p>No HTTP requests were made.</p> + </div> + {% else %} + <div class="metrics"> + <div class="metric"> + <span class="value">{{ collector.requestCount }}</span> + <span class="label">Total requests</span> + </div> + <div class="metric"> + <span class="value">{{ collector.errorCount }}</span> + <span class="label">HTTP errors</span> + </div> + </div> + <h2>Clients</h2> + <div class="sf-tabs"> + {% for name, client in collector.clients %} + <div class="tab {{ client.traces|length == 0 ? 'disabled' }}"> + <h3 class="tab-title">{{ name }} <span class="badge">{{ client.traces|length }}</span></h3> + <div class="tab-content"> + {% if client.traces|length == 0 %} + <div class="empty"> + <p>No requests were made with the "{{ name }}" service.</p> + </div> + {% else %} + <h4>Requests</h4> + {% for trace in client.traces %} + <table> + <thead> + <tr> + <th> + <span class="label">{{ trace.method }}</span> + </th> + <th class="full-width"> + {{ trace.url }} + {% if trace.options is not empty %} + {{ profiler_dump(trace.options, maxDepth=1) }} + {% endif %} + </th> + </tr> + </thead> + <tbody> + <tr> + <th> + {% if trace.http_code >= 500 %} + {% set responseStatus = 'error' %} + {% elseif trace.http_code >= 400 %} + {% set responseStatus = 'warning' %} + {% else %} + {% set responseStatus = 'success' %} + {% endif %} + <span class="label status-{{ responseStatus }}"> + {{ trace.http_code }} + </span> + </th> + <td> + {{ profiler_dump(trace.info, maxDepth=1) }} + </td> + </tr> + </tbody> + </table> + {% endfor %} + {% endif %} + </div> + </div> + {% endfor %} + {% endif %} + </div> +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig index f3d0f7cad4c14..d6e4d2af73f37 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/logger.html.twig @@ -201,11 + F438 201,11 @@ <tbody> {% for log in logs %} - {% set css_class = is_deprecation ? '' - : log.priorityName in ['CRITICAL', 'ERROR', 'ALERT', 'EMERGENCY'] ? 'status-error' + {% set css_class = not is_deprecation + ? log.priorityName in ['CRITICAL', 'ERROR', 'ALERT', 'EMERGENCY'] ? 'status-error' : log.priorityName == 'WARNING' ? 'status-warning' %} - <tr class="{{ css_class }}"{% if show_level %} data-filter-level="{{ log.priorityName|lower }}"{% endif %}{% if channel_is_defined %} data-filter-channel="{{ log.channel is not null ? log.channel : '' }}"{% endif %}> + <tr class="{{ css_class }}"{% if show_level %} data-filter-level="{{ log.priorityName|lower }}"{% endif %}{% if channel_is_defined %} data-filter-channel="{{ log.channel }}"{% endif %}> <td class="font-normal text-small" nowrap> {% if show_level %} <span class="colored text-bold">{{ log.priorityName }}</span> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig new file mode 100644 index 0000000000000..3c2e1bc8eb960 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig @@ -0,0 +1,202 @@ +{% extends '@WebProfiler/Profiler/layout.html.twig' %} + +{% block toolbar %} + {% set events = collector.events %} + + {% if events.messages|length %} + {% set icon %} + {% include('@WebProfiler/Icon/mailer.svg') %} + <span class="sf-toolbar-value">{{ events.messages|length }}</span> + {% endset %} + + {% set text %} + <div class="sf-toolbar-info-piece"> + <b>Sent messages</b> + <span class="sf-toolbar-status">{{ events.messages|length }}</span> + </div> + + {% for transport in events.transports %} + <div class="sf-toolbar-info-piece"> + <b>{{ transport }}</b> + <span class="sf-toolbar-status">{{ events.messages(transport)|length }}</span> + </div> + {% endfor %} + {% endset %} + + {{ include('@WebProfiler/Profiler/toolbar_item.html.twig', { 'link': profiler_url }) }} + {% endif %} +{% endblock %} + +{% block head %} + {{ parent() }} + <style type="text/css"> + /* utility classes */ + .m-t-0 { margin-top: 0 !important; } + .m-t-10 { margin-top: 10px !important; } + + /* basic grid */ + .row { + display: flex; + flex-wrap: wrap; + margin-right: -15px; + margin-left: -15px; + } + .col { + flex-basis: 0; + flex-grow: 1; + max-width: 100%; + position: relative; + width: 100%; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; + } + .col-4 { + flex: 0 0 33.333333%; + max-width: 33.333333%; + } + + /* small tabs */ + .sf-tabs-sm .tab-navigation li { + font-size: 14px; + padding: .3em .5em; + } + </style> +{% endblock %} + +{% block menu %} + {% set events = collector.events %} + + <span class="label {{ events.messages is empty ? 'disabled' }}"> + <span class="icon">{{ include('@WebProfiler/Icon/mailer.svg') }}</span> + + <strong>E-mails</strong> + {% if events.messages|length > 0 %} + <span class="count"> + <span>{{ events.messages|length }}</span> + </span> + {% endif %} + </span> +{% endblock %} + +{% block panel %} + {% set events = collector.events %} + + <h2>Emails</h2> + + {% if not events.messages|length %} + <div class="empty"> + <p>No emails were sent.</p> + </div> + {% endif %} + + <div class="metrics"> + {% for transport in events.transports %} + <div class="metric"> + <span class="value">{{ events.messages(transport)|length }}</span> + <span class="label">{{ events.messages(transport)|length == 1 ? 'message' : 'messages' }}</span> + </div> + {% endfor %} + </div> + + {% for transport in events.transports %} + <h3>{{ transport }}</h3> + + <div class="card-block"> + <div class="sf-tabs sf-tabs-sm"> + {% for event in events.events(transport) %} + {% set message = event.message %} + <div class="tab"> + <h3 class="tab-title">Email #{{ loop.index }} <small>({{ event.isQueued() ? 'queued' : 'sent' }})</small></h3> + <div class="tab-content"> + <div class="card"> + {% if message.headers is not defined %} + {# RawMessage instance #} + <div class="card-block"> + <pre class="prewrap" style="max-height: 600px">{{ message.toString() }}</pre> + </div> + {% else %} + {# Message instance #} + <div class="card-block"> + <span class="label">Subject</span> + <h2 class="m-t-10">{{ message.headers.get('subject').bodyAsString() ?? '(empty)' }}</h2> + </div> + + <div class="card-block"> + <div class="row"> + <div class="col col-4"> + <span class="label">From</span> + <pre class="prewrap">{{ (message.headers.get('from').bodyAsString() ?? '(empty)')|replace({'From:': ''}) }}</pre> + + <span class="label">To</span> + <pre class="prewrap">{{ (message.headers.get('to').bodyAsString() ?? '(empty)')|replace({'To:': ''}) }}</pre> + </div> + <div class="col"> + <span class="label">Headers</span> + <pre class="prewrap">{% for header in message.headers.all|filter(header => (header.name ?? '') not in ['Subject', 'From', 'To']) %} + {{- header.toString }} + {%~ endfor %}</pre> + </div> + </div> + </div> + + <div class="card-block"> + {% if message.htmlBody is defined %} + {# Email instance #} + <div class="sf-tabs sf-tabs-sm"> + <div class="tab"> + <h3 class="tab-title">HTML Content</h3> + <div class="tab-content"> + <pre class="prewrap" style="max-height: 600px"> + {%- if message.htmlCharset() %} + {{- message.htmlBody()|convert_encoding('UTF-8', message.htmlCharset()) }} + {%- else %} + {{- message.htmlBody() }} + {%- endif -%} + </pre> + </div> + </div> + <div class="tab"> + <h3 class="tab-title">Text Content</h3> + <div class="tab-content"> + <pre class="prewrap" style="max-height: 600px"> + {%- if message.textCharset() %} + {{- message.textBody()|convert_encoding('UTF-8', message.textCharset()) }} + {%- else %} + {{- message.textBody() }} + {%- endif -%} + </pre> + </div> + </div> + {% for attachment in message.attachments %} + <div class="tab"> + <h3 class="tab-title">Attachment #{{ loop.index }}</h3> + <div class="tab-content"> + <pre class="prewrap" style="max-height: 600px">{{ attachment.toString() }}</pre> + </div> + </div> + {% endfor %} + {% endif %} + <div class="tab"> + <h3 class="tab-title">Parts Hierarchy</h3> + <div class="tab-content"> + <pre class="prewrap" style="max-height: 600px">{{ message.body().asDebugString() }}</pre> + </div> + </div> + <div class="tab"> + <h3 class="tab-title">Raw</h3> + <div class="tab-content"> + <pre class="prewrap" style="max-height: 600px">{{ message.toString() }}</pre> + </div> + </div> + </div> + </div> + {% endif %} + </div> + </div> + </div> + {% endfor %} + </div> + </div> + {% endfor %} +{% endblock %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/memory.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/memory.html.twig index 268f8fdc7e3f6..1336a57a23398 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/memory.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/memory.html.twig @@ -2,21 +2,21 @@ {% block toolbar %} {% set icon %} - {% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' : '' %} + {% set status_color = (collector.memory / 1024 / 1024) > 50 ? 'yellow' %} {{ include('@WebProfiler/Icon/memory.svg') }} <span class="sf-toolbar-value">{{ '%.1f'|format(collector.memory / 1024 / 1024) }}</span> - <span class="sf-toolbar-label">MB</span> + <span class="sf-toolbar-label">MiB</span> {% endset %} {% set text %} <div class="sf-toolbar-info-piece"> <b>Peak memory usage</b> - <span>{{ '%.1f'|format(collector.memory / 1024 / 1024) }} MB</span> + <span>{{ '%.1f'|format(collector.memory / 1024 / 1024) }} MiB</span> </div> <div class="sf-toolbar-info-piece"> <b>PHP memory limit</b> - <span>{{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MB'|format(collector.memoryLimit / 1024 / 1024) }}</span> + <span>{{ collector.memoryLimit == -1 ? 'Unlimited' : '%.0f MiB'|format(collector.memoryLimit / 1024 / 1024) }}</span> </div> {% endset %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig index 779f1259edd01..b48aaa82e5787 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/messenger.html.twig @@ -45,7 +45,7 @@ {{ parent() }} <style> .message-item thead th { position: relative; cursor: pointer; user-select: none; padding-right: 35px; } - .message-item tbody tr td:first-child { width: 115px; } + .message-item tbody tr td:first-child { width: 170px; } .message-item .label { float: right; padding: 1px 5px; opacity: .75; margin-left: 5px; } .message-item .toggle-button { position: absolute; right: 6px; top: 6px; opacity: .5; pointer-events: none } @@ -119,8 +119,8 @@ <span class="label status-error">exception</span> {% endif %} <a class="toggle-button"> - <span class="icon icon-close">{{ include('@Twig/images/icon-minus-square.svg') }}</span> - <span class="icon icon-open">{{ include('@Twig/images/icon-plus-square.svg') }}</span> + <span class="icon icon-close">{{ include('@WebProfiler/images/icon-minus-square.svg') }}</span> + <span class="icon icon-open">{{ include('@WebProfiler/images/icon-plus-square.svg') }}</span> </a> </th> </tr> @@ -166,7 +166,7 @@ <td>{{ profiler_dump(dispatchCall.message.value, maxDepth=2) }}</td> </tr> <tr> - <td class="text-bold">Envelope stamps</td> + <td class="text-bold">Envelope stamps <span class="text-muted">when dispatching</span></td> <td> {% for item in dispatchCall.stamps %} {{ profiler_dump(item) }} @@ -175,6 +175,18 @@ {% endfor %} </td> </tr> + {% if dispatchCall.stamps_after_dispatch is defined %} + <tr> + <td class="text-bold">Envelope stamps <span class="text-muted">after dispatch</span></td> + <td> + {% for item in dispatchCall.stamps_after_dispatch %} + {{ profiler_dump(item) }} + {% else %} + <span class="text-muted">No items</span> + {% endfor %} + </td> + </tr> + {% endif %} {% if dispatchCall.exception is defined %} <tr> <td class="text-bold">Exception</td> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.css.twig index ca46eafb9a0e1..b64b6ff869280 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.css.twig @@ -1,15 +1,3 @@ -/* Variables */ - -.sf-profiler-timeline { - --color-default: #777; - --color-section: #999; - --color-event-listener: #00B8F5; - --color-template: #66CC00; - --color-doctrine: #FF6633; - --color-messenger-middleware: #BDB81E; - --color-controller-argument-value-resolver: #8c5de6; -} - /* Legend */ .sf-profiler-timeline .legends .timeline-category { @@ -31,14 +19,6 @@ display: inline-block; } -.sf-profiler-timeline .legends .{{ classnames.default|raw }} { border-color: var(--color-default); } -.sf-profiler-timeline .legends .{{ classnames.section|raw }} { border-color: var(--color-section); } -.sf-profiler-timeline .legends .{{ classnames.event_listener|raw }} { border-color: var(--color-event-listener); } -.sf-profiler-timeline .legends .{{ classnames.template|raw }} { border-color: var(--color-template); } -.sf-profiler-timeline .legends .{{ classnames.doctrine|raw }} { border-color: var(--color-doctrine); } -.sf-profiler-timeline .legends .{{ classnames['messenger.middleware']|raw }} { border-color: var(--color-messenger-middleware); } -.sf-profiler-timeline .legends .{{ classnames['controller.argument_value_resolver']|raw }} { border-color: var(--color-controller-argument-value-resolver); } - .timeline-graph { margin: 1em 0; width: 100%; @@ -82,24 +62,3 @@ .timeline-graph .timeline-period { stroke-width: 0; } -.timeline-graph .{{ classnames.default|raw }} .timeline-period { - fill: var(--color-default); -} -.timeline-graph .{{ classnames.section|raw }} .timeline-period { - fill: var(--color-section); -} -.timeline-graph .{{ classnames.event_listener|raw }} .timeline-period { - fill: var(--color-event-listener); -} -.timeline-graph .{{ classnames.template|raw }} .timeline-period { - fill: var(--color-template); -} -.timeline-graph .{{ classnames.doctrine|raw }} .timeline-period { - fill: var(--color-doctrine); -} -.timeline-graph .{{ classnames['messenger.middleware']|raw }} .timeline-period { - fill: var(--color-messenger-middleware); -} -.timeline-graph .{{ classnames['controller.argument_value_resolver']|raw }} .timeline-period { - fill: var(--color-controller-argument-value-resolver); -} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig index 20b72098dc2a8..0ed3ddc09b512 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.html.twig @@ -2,21 +2,11 @@ {% import _self as helper %} -{% set classnames = { - 'default': 'timeline-category-default', - 'section': 'timeline-category-section', - 'event_listener': 'timeline-category-event-listener', - 'template': 'timeline-category-template', - 'doctrine': 'timeline-category-doctrine', - 'messenger.middleware': 'timeline-category-messenger-middleware', - 'controller.argument_value_resolver': 'timeline-category-controller-argument-value-resolver', -} %} - {% block toolbar %} {% set has_time_events = collector.events|length > 0 %} {% set total_time = has_time_events ? '%.0f'|format(collector.duration) : 'n/a' %} {% set initialization_time = collector.events|length ? '%.0f'|format(collector.inittime) : 'n/a' %} - {% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' : '' %} + {% set status_color = has_time_events and collector.duration > 1000 ? 'yellow' %} {% set icon %} {{ include('@WebProfiler/Icon/time.svg') }} @@ -62,7 +52,7 @@ {% if profile.collectors.memory %} <div class="metric"> - <span class="value">{{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} <span class="unit">MB</span></span> + <span class="value">{{ '%.2f'|format(profile.collectors.memory.memory / 1024 / 1024) }} <span class="unit">MiB</span></span> <span class="label">Peak memory usage</span> </div> {% endif %} @@ -128,7 +118,7 @@ </h3> {% endif %} - {{ helper.display_timeline(token, classnames, collector.events, collector.events.__section__.origin) }} + {{ helper.display_timeline(token, collector.events, collector.events.__section__.origin) }} {% if profile.children|length %} <p class="help">Note: sections with a striped background correspond to sub-requests.</p> @@ -142,7 +132,7 @@ <small>{{ events.__section__.duration }} ms</small> </h4> - {{ helper.display_timeline(child.token, classnames, events, collector.events.__section__.origin) }} + {{ helper.display_timeline(child.token, events, collector.events.__section__.origin) }} {% endfor %} {% endif %} @@ -154,7 +144,7 @@ </defs> </svg> <style type="text/css"> -{% include '@WebProfiler/Collector/time.css.twig' with classnames %} +{% include '@WebProfiler/Collector/time.css.twig' %} </style> <script> {% include '@WebProfiler/Collector/time.js' %} @@ -202,16 +192,19 @@ {% endautoescape %} {% endmacro %} -{% macro display_timeline(token, classnames, events, origin) %} +{% macro display_timeline(token, events, origin) %} {% import _self as helper %} <div class="sf-profiler-timeline"> <div id="legend-{{ token }}" class="legends"></div> <svg id="timeline-{{ token }}" class="timeline-graph"></svg> <script>{% autoescape 'js' %} window.addEventListener('load', function onLoad() { + const theme = new Theme(); + new TimelineEngine( + theme, new SvgRenderer(document.getElementById('timeline-{{ token }}')), - new Legend(document.getElementById('legend-{{ token }}'), {{ classnames|json_encode|raw }}), + new Legend(document.getElementById('legend-{{ token }}'), theme), document.getElementById('threshold'), {{ helper.dump_request_data(token, events, origin) }} ); diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.js b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.js index e21a9425fd9b1..588a9d22ed350 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.js +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/time.js @@ -2,6 +2,7 @@ class TimelineEngine { /** + * @param {Theme} theme * @param {Renderer} renderer * @param {Legend} legend * @param {Element} threshold @@ -9,7 +10,8 @@ class TimelineEngine { * @param {Number} eventHeight * @param {Number} horizontalMargin */ - constructor(renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) { + constructor(theme, renderer, legend, threshold, request, eventHeight = 36, horizontalMargin = 10) { + this.theme = theme; this.renderer = renderer; this.legend = legend; this.threshold = threshold; @@ -81,7 +83,7 @@ class TimelineEngine { const lines = periods.map(period => this.createPeriod(period, category)); const label = this.createLabel(this.getShortName(name), duration, memory, periods[0]); const title = this.renderer.createTitle(name); - const group = this.renderer.group([title, border, label].concat(lines), this.legend.getClassname(event.category)); + const group = this.renderer.group([title, border, label].concat(lines), this.theme.getCategoryColor(event.category)); event.elements = Object.assign(event.elements || {}, { group, label, border }); @@ -92,7 +94,7 @@ class TimelineEngine { createLabel(name, duration, memory, period) { const label = this.renderer.createText(name, period.start * this.scale, this.labelY, 'timeline-label'); - const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} Mb`, 'timeline-sublabel'); + const sublabel = this.renderer.createTspan(` ${duration} ms / ${memory} MiB`, 'timeline-sublabel'); label.appendChild(sublabel); @@ -100,7 +102,7 @@ class TimelineEngine { } createPeriod(period, category) { - const timeline = this.renderer.createPath(null, 'timeline-period'); + const timeline = this.renderer.createPath(null, 'timeline-period', this.theme.getCategoryColor(category)); period.draw = category === 'section' ? this.renderer.setSectionLine : this.renderer.setPeriodLine; period.elements = Object.assign(period.elements || {}, { timeline }); @@ -213,14 +215,15 @@ class TimelineEngine { } class Legend { - constructor(element, classnames) { + constructor(element, theme) { this.element = element; - this.classnames = classnames; + this.theme = theme; this.toggle = this.toggle.bind(this); this.createCategory = this.createCategory.bind(this); - this.categories = Array.from(Object.keys(classnames)).map(this.createCategory); + this.categories = []; + this.theme.getDefaultCategories().forEach(this.createCategory); } add(category) { @@ -229,8 +232,8 @@ class Legend { createCategory(category) { const element = document.createElement('button'); - - element.className = `timeline-category ${this.getClassname(category)} active`; + element.className = `timeline-category active`; + element.style.borderColor = this.theme.getCategoryColor(category); element.innerText = category; element.value = category; element.type = 'button'; @@ -238,6 +241,8 @@ class Legend { this.element.appendChild(element); + this.categories.push(element); + return element; } @@ -255,23 +260,6 @@ class Legend { return this.categories.find(element => element.value === category) || this.createCategory(category); } - getClassname(category) { - return this.classnames[category] || ''; - } - - getSectionClassname() { - return this.classnames.section; - } - - getDefaultClassname() { - return this.classnames.default; - } - - getStandardClassenames() { - return Array.from(Object.values(this.classnames)) - .filter(className => className !== this.getSectionClassname()); - } - emit(name) { this.element.dispatchEvent(new Event(name)); } @@ -390,13 +378,17 @@ class SvgRenderer { return element; } - createPath(path = null, className = null) { + createPath(path = null, className = null, color = null) { const element = this.create('path', className); if (path) { element.setAttribute('d', path); } + if (color) { + element.setAttribute('fill', color); + } + return element; } @@ -410,3 +402,55 @@ class SvgRenderer { return element; } } + +class Theme { + constructor(element) { + this.reservedCategoryColors = { + 'default': '#777', + 'section': '#999', + 'event_listener': '#00b8f5', + 'template': '#66cc00', + 'doctrine': '#ff6633', + 'messenger_middleware': '#bdb81e', + 'controller.argument_value_resolver': '#8c5de6', + }; + + this.customCategoryColors = [ + '#dbab09', // dark yellow + '#ea4aaa', // pink + '#964b00', // brown + '#22863a', // dark green + '#0366d6', // dark blue + '#17a2b8', // teal + ]; + + this.getCategoryColor = this.getCategoryColor.bind(this); + this.getDefaultCategories = this.getDefaultCategories.bind(this); + } + + getDefaultCategories() { + return Object.keys(this.reservedCategoryColors); + } + + getCategoryColor(category) { + return this.reservedCategoryColors[category] || this.getRandomColor(category); + } + + getRandomColor(category) { + // instead of pure randomness, colors are assigned deterministically based on the + // category name, to ensure that each custom category always displays the same color + return this.customCategoryColors[this.hash(category) % this.customCategoryColors.length]; + } + + // copied from https://github.com/darkskyapp/string-hash + hash(string) { + var hash = 5381; + var i = string.length; + + while(i) { + hash = (hash * 33) ^ string.charCodeAt(--i); + } + + return hash >>> 0; + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig index b37da94681167..a8a5c273656b5 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/translation.html.twig @@ -13,7 +13,7 @@ {% set text %} <div class="sf-toolbar-info-piece"> - <b>Locale</b> + <b>Default locale</b> <span class="sf-toolbar-status"> {{ collector.locale|default('-') }} </span> @@ -61,7 +61,7 @@ <div class="metrics"> <div class="metric"> <span class="value">{{ collector.locale|default('-') }}</span> - <span class="label">Locale</span> + <span class="label">Default locale</span> </div> <div class="metric"> <span class="value">{{ collector.fallbackLocales|join(', ')|default('-') }}</span> @@ -126,7 +126,7 @@ </div> {% else %} {% block fallback_messages %} - {{ helper.render_table(messages_fallback) }} + {{ helper.render_table(messages_fallback, true) }} {% endblock %} {% endif %} </div> @@ -162,11 +162,14 @@ {% endblock %} -{% macro render_table(messages) %} +{% macro render_table(messages, is_fallback) %} <table data-filters> <thead> <tr> <th data-filter="locale">Locale</th> + {% if is_fallback %} + <th>Fallback locale</th> + {% endif %} <th data-filter="domain">Domain</th> <th>Times used</th> <th>Message ID</th> @@ -177,6 +180,9 @@ {% for message in messages %} <tr data-filter-locale="{{ message.locale }}" data-filter-domain="{{ message.domain }}"> <td class="font-normal text-small nowrap">{{ message.locale }}</td> + {% if is_fallback %} + <td class="font-normal text-small nowrap">{{ message.fallbackLocale|default('-') }}</td> + {% endif %} <td class="font-normal text-small text-bold nowrap">{{ message.domain }}</td> <td class="font-normal text-small nowrap">{{ message.count }}</td> <td> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig index f1da1f714fb26..f3b7b7656e87c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/validator.html.twig @@ -2,7 +2,7 @@ {% block toolbar %} {% if collector.violationsCount > 0 or collector.calls|length %} - {% set status_color = collector.violationsCount ? 'red' : '' %} + {% set status_color = collector.violationsCount ? 'red' %} {% set icon %} {{ include('@WebProfiler/Icon/validator.svg') }} <span class="sf-toolbar-value"> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg new file mode 100644 index 0000000000000..e6b1fb2fe903c --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/http-client.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAA" d="M20.4 12c-1 0-1.8.6-2.2 1.4l-2.6-.9c.1-.3.1-.5.1-.8 0-1.2-.6-2.2-1.5-2.9l1.5-2.6c.3.1.6.2 1 .2 1.4 0 2.5-1.1 2.5-2.5s-1.1-2.5-2.5-2.5-2.5 1.1-2.5 2.5c0 .8.4 1.5.9 1.9l-1.5 2.6c-.5-.3-1-.4-1.6-.4-.9 0-1.7.3-2.3.9L7.4 6.6c.3-.4.5-.9.5-1.5 0-1.4-1.1-2.5-2.5-2.5S2.7 3.7 2.7 5.1s1.1 2.5 2.5 2.5c.6 0 1.1-.2 1.5-.5L9 9.4c-.5.6-.8 1.4-.8 2.3 0 .7.2 1.4.6 2l-3.9 3.8c-.4-.3-.9-.5-1.5-.5C2 17 .9 18.1.9 19.5S2.2 22 3.6 22s2.5-1.1 2.5-2.5c0-.5-.2-1-.5-1.5l3.8-3.7c.7.7 1.6 1.1 2.6 1.1h.2l.4 2.4c-1 .3-1.7 1.3-1.7 2.4 0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5-1.1-2.5-2.5-2.5l-.4-2.5c1-.3 1.9-1 2.3-2l2.6.9v.4c0 1.4 1.1 2.5 2.5 2.5s2.5-1.1 2.5-2.5c.1-1.4-1.1-2.5-2.5-2.5z"/></svg> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/mailer.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/mailer.svg new file mode 100644 index 0000000000000..ed649d0681073 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Icon/mailer.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="#AAAAAA" d="M22,4.9C22,3.9,21.1,3,20.1,3H3.9C2.9,3,2,3.9,2,4.9v13.1C2,19.1,2.9,20,3.9,20h16.1c1.1,0,1.9-0.9,1.9-1.9V4.9z M8.3,14.1l-3.1,3.1c-0.2,0.2-0.5,0.3-0.7,0.3S4,17.4,3.8,17.2c-0.4-0.4-0.4-1,0-1.4l3.1-3.1c0.4-0.4,1-0.4,1.4,0S8.7,13.7,8.3,14.1z M20.4,17.2c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3l-3.1-3.1c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l3.1,3.1C20.8,16.2,20.8,16.8,20.4,17.2z M20.4,7.2l-7.6,7.6c-0.2,0.2-0.5,0.3-0.7,0.3s-0.5-0.1-0.7-0.3L3.8,7.2c-0.4-0.4-0.4-1,0-1.4s1-0.4,1.4,0l6.9,6.9L19,5.8c0.4-0.4,1-0.4,1.4,0S20.8,6.8,20.4,7.2z"/></svg> diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig index 580b3b5b0e570..9426753b00e57 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/base.html.twig @@ -4,17 +4,17 @@ <meta charset="{{ _charset }}" /> <meta name="robots" content="noindex,nofollow" /> <meta name="viewport" content="width=device-width,initial-scale=1" /> - <title>Symfony Profiler + {% block title %}Symfony Profiler{% endblock %} {% block head %} - {% endblock %} - diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig index 0227532e1208a..43404393ec360 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/info.html.twig @@ -4,7 +4,7 @@ 'no_token' : { status: 'error', title: (token|default('') == 'latest') ? 'There are no profiles' : 'Token not found', - message: (token|default('') == 'latest') ? 'No profiles found in the database.' : 'Token "' ~ token|default('') ~ '" was not found in the database.' + message: (token|default('') == 'latest') ? 'No profiles found.' : 'Token "' ~ token|default('') ~ '" not found.' } } %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig index bbd525d095dde..c52c5732b042c 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/layout.html.twig @@ -123,7 +123,7 @@ {%- endif -%} {%- endset %} {% if menu is not empty %} -
  • +
  • {{ menu|raw }}
  • {% endif %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig index d737ab0e063ad..db21abf886c85 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/profiler.css.twig @@ -39,6 +39,7 @@ --highlight-default: #222222; --highlight-keyword: #a71d5d; --highlight-string: #183691; + --highlight-selected-line: rgba(255, 255, 153, 0.5); --base-0: #fff; --base-1: #f5f5f5; --base-2: #e0e0e0; @@ -46,6 +47,8 @@ --base-4: #666; --base-5: #444; --base-6: #222; + --card-label-background: #eee; + --card-label-color: var(--base-6); } .theme-dark { @@ -78,6 +81,7 @@ --highlight-default: var(--base-6); --highlight-keyword: #ff413c; --highlight-string: #70a6fd; + --highlight-selected-line: rgba(14, 14, 14, 0.5); --base-0: #2e3136; --base-1: #444; --base-2: #666; @@ -85,6 +89,8 @@ --base-4: #666; --base-5: #e0e0e0; --base-6: #f5f5f5; + --card-label-background: var(--tab-active-background); + --card-label-color: var(--tab-active-color); } {# Basic styles @@ -436,8 +442,8 @@ table tbody td.num-col { margin-top: 0; } .card .label { - background-color: #EEE; - color: var(--base-6); + background-color: var(--card-label-background); + color: var(--card-label-color); } {# Status @@ -1087,15 +1093,15 @@ table.logs .metadata { padding: 0; } #collector-content .sf-validator .trace li.selected { - background: rgba(255, 255, 153, 0.5); + background: var(--highlight-selected-line); } {# Messenger panel ========================================================================= #} #collector-content .message-bus .trace { - border: 1px solid #DDD; - background: #FFF; + border: var(--border); + background: var(--base-0); padding: 10px; margin: 0.5em 0; overflow: auto; @@ -1108,7 +1114,7 @@ table.logs .metadata { padding: 0; } #collector-content .message-bus .trace li.selected { - background: rgba(255, 255, 153, 0.5); + background: var(--highlight-selected-line); } {# Dump panel diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig index dd09415568d70..5d0e3c928a10e 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar.css.twig @@ -54,6 +54,7 @@ text-align: left; text-transform: none; z-index: 99999; + direction: ltr; /* neutralize the aliasing defined by external CSS styles */ -webkit-font-smoothing: subpixel-antialiased; @@ -99,6 +100,7 @@ .sf-toolbar-block > a:hover { display: block; text-decoration: none; + background-color: transparent; color: inherit; } @@ -237,6 +239,7 @@ div.sf-toolbar .sf-toolbar-block a:hover { padding: 0 10px; } .sf-toolbar-block-request .sf-toolbar-info-piece a { + background-color: transparent; text-decoration: none; } .sf-toolbar-block-request .sf-toolbar-info-piece a:hover { @@ -321,7 +324,7 @@ div.sf-toolbar .sf-toolbar-block a:hover { .sf-toolbar-block.hover .sf-toolbar-info { display: block; padding: 10px; - max-width: 480px; + max-width: 525px; max-height: 480px; word-wrap: break-word; overflow: hidden; @@ -540,6 +543,6 @@ div.sf-toolbar .sf-toolbar-block a:hover { /***** Media query print: Do not print the Toolbar. *****/ @media print { .sf-toolbar { - display: none; + display: none !important; } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig index e340d89f96b9e..f6e4fde06f643 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_js.html.twig @@ -1,4 +1,4 @@ -
    +
    {{ include('@WebProfiler/Profiler/base_js.html.twig') }} {{ include('@WebProfiler/Profiler/toolbar.css.twig') }} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_redirect.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_redirect.html.twig index 35b6e90eb56ae..18d43b2253ecf 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_redirect.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/toolbar_redirect.html.twig @@ -1,4 +1,4 @@ -{% extends '@Twig/layout.html.twig' %} +{% extends '@WebProfiler/Profiler/base.html.twig' %} {% block title 'Redirection Intercepted' %} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-minus-square.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-minus-square.svg new file mode 100644 index 0000000000000..471c2741c7fd7 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-minus-square.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-plus-square.svg b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-plus-square.svg new file mode 100644 index 0000000000000..2f5c3b3583076 --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/images/icon-plus-square.svg @@ -0,0 +1 @@ + diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php index 79b289e5a1e64..661717dfcad2b 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Controller/ProfilerControllerTest.php @@ -11,26 +11,123 @@ namespace Symfony\Bundle\WebProfilerBundle\Tests\Controller; -use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\KernelBrowser; +use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\WebProfilerBundle\Controller\ProfilerController; use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator; +use Symfony\Bundle\WebProfilerBundle\Tests\Functional\WebProfilerBundleKernel; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\DataCollector\DumpDataCollector; +use Symfony\Component\HttpKernel\DataCollector\ExceptionDataCollector; +use Symfony\Component\HttpKernel\DataCollector\RequestDataCollector; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\HttpKernel\Profiler\Profiler; +use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Twig\Environment; +use Twig\Loader\LoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; -class ProfilerControllerTest extends TestCase +class ProfilerControllerTest extends WebTestCase { + public function testHomeActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->homeAction(); + } + + public function testHomeActionRedirect() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/'); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertSame('/_profiler/empty/search/results?limit=10', $client->getResponse()->getTargetUrl()); + } + + public function testPanelActionWithLatestTokenWhenNoTokensExist() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/latest'); + + $this->assertStringContainsString('No profiles found.', $client->getResponse()->getContent()); + } + + public function testPanelActionWithLatestToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $client->request('GET', '/_profiler/latest'); + + $this->assertStringContainsString('kernel:homepageController', $client->getResponse()->getContent()); + } + + public function testPanelActionWithoutValidToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/this-token-does-not-exist'); + + $this->assertStringContainsString('Token "this-token-does-not-exist" not found.', $client->getResponse()->getContent()); + } + + public function testPanelActionWithWrongPanel() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $client->request('GET', '/_profiler/latest?panel=this-panel-does-not-exist'); + + $this->assertSame(404, $client->getResponse()->getStatusCode()); + } + + public function testPanelActionWithValidPanelAndToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $crawler = $client->request('GET', '/_profiler/latest?panel=router'); + + $this->assertSame('_', $crawler->filter('.metrics .metric .value')->eq(0)->text()); + } + + public function testToolbarActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->toolbarAction(Request::create('/_wdt/foo-token'), null); + } + /** * @dataProvider getEmptyTokenCases */ - public function testEmptyToken($token) + public function testToolbarActionWithEmptyToken($token) { - $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + $profiler = $this->createMock(Profiler::class); $controller = new ProfilerController($urlGenerator, $profiler, $twig, []); @@ -52,12 +149,9 @@ public function getEmptyTokenCases() */ public function testOpeningDisallowedPaths($path, $isAllowed) { - $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + $profiler = $this->createMock(Profiler::class); $controller = new ProfilerController($urlGenerator, $profiler, $twig, [], null, __DIR__.'/../..'); @@ -88,19 +182,14 @@ public function getOpenFileCases() */ public function testReturns404onTokenNotFound($withCsp) { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); + $twig = $this->createMock(Environment::class); + $profiler = $this->createMock(Profiler::class); $profiler ->expects($this->exactly(2)) ->method('loadProfile') ->willReturnCallback(function ($token) { - if ('found' == $token) { - return new Profile($token); - } + return 'found' == $token ? new Profile($token) : null; }) ; @@ -113,16 +202,39 @@ public function testReturns404onTokenNotFound($withCsp) $this->assertEquals(404, $response->getStatusCode()); } + public function testSearchBarActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->searchBarAction(Request::create('/_profiler/search_bar')); + } + + public function testSearchBarActionDefaultPage() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $crawler = $client->request('GET', '/_profiler/search_bar'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + foreach (['ip', 'status_code', 'url', 'token', 'start', 'end'] as $searchCriteria) { + $this->assertSame('', $crawler->filter(sprintf('form input[name="%s"]', $searchCriteria))->text()); + } + } + /** * @dataProvider provideCspVariants */ - public function testSearchResult($withCsp) + public function testSearchResultsAction($withCsp) { - $twig = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); - $profiler = $this - ->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); + $twig = $this->createMock(Environment::class); + $profiler = $this->createMock(Profiler::class); $controller = $this->createController($profiler, $twig, $withCsp); @@ -173,12 +285,75 @@ public function testSearchResult($withCsp) 'limit' => 2, 'panel' => null, 'request' => $request, + 'csp_script_nonce' => $withCsp ? 'dummy_nonce' : null, + 'csp_style_nonce' => $withCsp ? 'dummy_nonce' : null, ])); $response = $controller->searchResultsAction($request, 'empty'); 10000 $this->assertEquals(200, $response->getStatusCode()); } + public function testSearchActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->searchBarAction(Request::create('/_profiler/search')); + } + + public function testSearchActionWithToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/'); + $token = $client->getResponse()->headers->get('x-debug-token'); + $client->request('GET', '/_profiler/search?token='.$token); + + $this->assertSame(302, $client->getResponse()->getStatusCode()); + $this->assertSame('/_profiler/'.$token, $client->getResponse()->getTargetUrl()); + } + + public function testSearchActionWithoutToken() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + $client->followRedirects(); + + $client->request('GET', '/'); + $token = $client->getResponse()->headers->get('x-debug-token'); + $client->request('GET', '/_profiler/search?ip=&method=GET&status_code=&url=&token=&start=&end=&limit=10'); + + $this->assertStringContainsString('results found', $client->getResponse()->getContent()); + $this->assertStringContainsString(sprintf('%s', $token, $token), $client->getResponse()->getContent()); + } + + public function testPhpinfoActionWithProfilerDisabled() + { + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('The profiler must be enabled.'); + + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); + $twig = $this->createMock(Environment::class); + + $controller = new ProfilerController($urlGenerator, null, $twig, []); + $controller->phpinfoAction(Request::create('/_profiler/phpinfo')); + } + + public function testPhpinfoAction() + { + $kernel = new WebProfilerBundleKernel(); + $client = new KernelBrowser($kernel); + + $client->request('GET', '/_profiler/phpinfo'); + + $this->assertStringContainsString('PHP License', $client->getResponse()->getContent()); + } + public function provideCspVariants() { return [ @@ -187,16 +362,109 @@ public function provideCspVariants() ]; } - private function createController($profiler, $twig, $withCSP) + /** + * @dataProvider defaultPanelProvider + */ + public function testDefaultPanel(string $expectedPanel, Profile $profile) + { + $profiler = $this->createMock(Profiler::class); + $profiler + ->expects($this->atLeastOnce()) + ->method('loadProfile') + ->with($profile->getToken()) + ->willReturn($profile); + + $collectorsNames = array_keys($profile->getCollectors()); + + $profiler + ->expects($this->atLeastOnce()) + ->method('has') + ->with($this->logicalXor(...$collectorsNames)) + ->willReturn(true); + + $expectedTemplate = 'expected_template.html.twig'; + + if (Environment::MAJOR_VERSION > 1) { + $loader = $this->createMock(LoaderInterface::class); + $loader + ->expects($this->atLeastOnce()) + ->method('exists') + ->with($this->logicalXor($expectedTemplate, 'other_template.html.twig')) + ->willReturn(true); + } else { + $loader = $this->createMock(SourceContextLoaderInterface::class); + } + + $twig = $this->createMock(Environment::class); + $twig + ->expects($this->atLeastOnce()) + ->method('getLoader') + ->willReturn($loader); + $twig + ->expects($this->once()) + ->method('render') + ->with($expectedTemplate); + + $this + ->createController($profiler, $twig, false, array_map(function (string $collectorName) use ($expectedPanel, $expectedTemplate): array { + if ($collectorName === $expectedPanel) { + return [$expectedPanel, $expectedTemplate]; + } + + return [$collectorName, 'other_template.html.twig']; + }, $collectorsNames)) + ->panelAction(new Request(), $profile->getToken()); + } + + public function defaultPanelProvider(): \Generator + { + // Test default behavior + $profile = new Profile('xxxxxx'); + $profile->addCollector($requestDataCollector = new RequestDataCollector()); + yield [$requestDataCollector->getName(), $profile]; + + // Test exception + $profile = new Profile('xxxxxx'); + $profile->addCollector($exceptionDataCollector = new ExceptionDataCollector()); + $exceptionDataCollector->collect(new Request(), new Response(), new \DomainException()); + yield [$exceptionDataCollector->getName(), $profile]; + + // Test exception priority + $dumpDataCollector = $this->createMock(DumpDataCollector::class); + $dumpDataCollector + ->expects($this->atLeastOnce()) + ->method('getName') + ->willReturn('dump'); + $dumpDataCollector + ->expects($this->atLeastOnce()) + ->method('getDumpsCount') + ->willReturn(1); + $profile = new Profile('xxxxxx'); + $profile->setCollectors([$exceptionDataCollector, $dumpDataCollector]); + yield [$exceptionDataCollector->getName(), $profile]; + + // Test exception priority when defined afterwards + $profile = new Profile('xxxxxx'); + $profile->setCollectors([$dumpDataCollector, $exceptionDataCollector]); + yield [$exceptionDataCollector->getName(), $profile]; + + // Test dump + $profile = new Profile('xxxxxx'); + $profile->addCollector($dumpDataCollector); + yield [$dumpDataCollector->getName(), $profile]; + } + + private function createController($profiler, $twig, $withCSP, array $templates = []): ProfilerController { - $urlGenerator = $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); if ($withCSP) { - $nonceGenerator = $this->getMockBuilder('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator')->getMock(); + $nonceGenerator = $this->createMock(NonceGenerator::class); + $nonceGenerator->method('generate')->willReturn('dummy_nonce'); - return new ProfilerController($urlGenerator, $profiler, $twig, [], new ContentSecurityPolicyHandler($nonceGenerator)); + return new ProfilerController($urlGenerator, $profiler, $twig, $templates, new ContentSecurityPolicyHandler($nonceGenerator)); } - return new ProfilerController($urlGenerator, $profiler, $twig, []); + return new ProfilerController($urlGenerator, $profiler, $twig, $templates); } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php index acccc7cbfb6d2..fbaf2f7965d05 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Csp/ContentSecurityPolicyHandlerTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\WebProfilerBundle\Csp\ContentSecurityPolicyHandler; +use Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; @@ -41,7 +42,7 @@ public function testOnKernelResponse($nonce, $expectedNonce, Request $request, R $this->assertFalse($response->headers->has('X-SymfonyProfiler-Style-Nonce')); foreach ($expectedCsp as $header => $value) { - $this->assertSame($value, $response->headers->get($header)); + $this->assertSame($value, $response->headers->get($header), $header); } } @@ -133,6 +134,20 @@ public function provideRequestAndResponsesForOnKernelResponse() $this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'']), ['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; style-src \'self\' domain-report-only.com \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], ], + [ + $nonce, + ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], + $this->createRequest(), + $this->createResponse(['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\'']), + ['Content-Security-Policy' => 'default-src \'self\' domain.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'self\' domain-report-only.com; script-src \'self\' \'unsafe-inline\'; script-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'self\' \'unsafe-inline\'; style-src-elem \'self\' \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], + ], + [ + $nonce, + ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], + $this->createRequest(), + $this->createResponse(['Content-Security-Policy' => 'default-src \'none\'', 'Content-Security-Policy-Report-Only' => 'default-src \'none\'']), + ['Content-Security-Policy' => 'default-src \'none\'; script-src \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'Content-Security-Policy-Report-Only' => 'default-src \'none\'; script-src \'unsafe-inline\' \'nonce-'.$nonce.'\'; style-src \'unsafe-inline\' \'nonce-'.$nonce.'\'', 'X-Content-Security-Policy' => null], + ], [ $nonce, ['csp_script_nonce' => $nonce, 'csp_style_nonce' => $nonce], @@ -196,7 +211,7 @@ private function createResponse(array $headers = []) private function mockNonceGenerator($value) { - $generator = $this->getMockBuilder('Symfony\Bundle\WebProfilerBundle\Csp\NonceGenerator')->getMock(); + $generator = $this->createMock(NonceGenerator::class); $generator->expects($this->any()) ->method('generate') diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/ConfigurationTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/ConfigurationTest.php index 79e09054bbcae..a0bc7f43c0668 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/ConfigurationTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/ConfigurationTest.php @@ -20,23 +20,76 @@ class ConfigurationTest extends TestCase /** * @dataProvider getDebugModes */ - public function testConfigTree($options, $results) + public function testConfigTree(array $options, array $expectedResult) { $processor = new Processor(); $configuration = new Configuration(); $config = $processor->processConfiguration($configuration, [$options]); - $this->assertEquals($results, $config); + $this->assertEquals($expectedResult, $config); } public function getDebugModes() { return [ - [[], ['intercept_redirects' => false, 'toolbar' => false, 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt']], - [['intercept_redirects' => true], ['intercept_redirects' => true, 'toolbar' => false, 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt']], - [['intercept_redirects' => false], ['intercept_redirects' => false, 'toolbar' => false, 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt']], - [['toolbar' => true], ['intercept_redirects' => false, 'toolbar' => true, 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt']], - [['excluded_ajax_paths' => 'test'], ['intercept_redirects' => false, 'toolbar' => false, 'excluded_ajax_paths' => 'test']], + [ + 'options' => [], + 'expectedResult' => [ + 'intercept_redirects' => false, + 'toolbar' => false, + 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt', + ], + ], + [ + 'options' => ['toolbar' => true], + 'expectedResult' => [ + 'intercept_redirects' => false, + 'toolbar' => true, + 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt', + ], + ], + [ + 'options' => ['excluded_ajax_paths' => 'test'], + 'expectedResult' => [ + 'intercept_redirects' => false, + 'toolbar' => false, + 'excluded_ajax_paths' => 'test', + ], + ], + ]; + } + + /** + * @dataProvider getInterceptRedirectsConfiguration + */ + public function testConfigTreeUsingInterceptRedirects(bool $interceptRedirects, array $expectedResult) + { + $processor = new Processor(); + $configuration = new Configuration(); + $config = $processor->processConfiguration($configuration, [['intercept_redirects' => $interceptRedirects]]); + + $this->assertEquals($expectedResult, $config); + } + + public function getInterceptRedirectsConfiguration() + { + return [ + [ + 'interceptRedirects' => true, + 'expectedResult' => [ + 'intercept_redirects' => true, + 'toolbar' => false, + 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt', + ], + ], + [ + 'interceptRedirects' => false, + 'expectedResult' => [ + 'intercept_redirects' => false, + 'toolbar' => false, + 'excluded_ajax_paths' => '^/((index|app(_[\w]+)?)\.php/)?_wdt', + ], + ], ]; } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php index cfbee00bd0e9d..bb666f4316026 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/DependencyInjection/WebProfilerExtensionTest.php @@ -17,9 +17,10 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\Definition; use Symfony\Component\DependencyInjection\Reference; -use Symfony\Component\ErrorRenderer\ErrorRenderer\HtmlErrorRenderer; +use Symfony\Component\ErrorHandler\ErrorRenderer\HtmlErrorRenderer; use Symfony\Component\EventDispatcher\DependencyInjection\RegisterListenersPass; use Symfony\Component\EventDispatcher\EventDispatcher; +use Symfony\Component\HttpKernel\KernelInterface; class WebProfilerExtensionTest extends TestCase { @@ -47,14 +48,14 @@ public static function assertSaneContainer(Container $container, $message = '', self::assertEquals([], $errors, $message); } - protected function setUp() + protected function setUp(): void { parent::setUp(); - $this->kernel = $this->getMockBuilder('Symfony\\Component\\HttpKernel\\KernelInterface')->getMock(); + $this->kernel = $this->createMock(KernelInterface::class); $this->container = new ContainerBuilder(); - $this->container->register('error_renderer.renderer.html', HtmlErrorRenderer::class); + $this->container->register('error_handler.error_renderer.html', HtmlErrorRenderer::class)->setPublic(true); $this->container->register('event_dispatcher', EventDispatcher::class)->setPublic(true); $this->container->register('router', $this->getMockClass('Symfony\\Component\\Routing\\RouterInterface'))->setPublic(true); $this->container->register('twig', 'Twig\Environment')->setPublic(true); @@ -75,7 +76,7 @@ protected function setUp() $this->container->addCompilerPass(new RegisterListenersPass()); } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -92,36 +93,101 @@ public function testDefaultConfig($debug) $extension = new WebProfilerExtension(); $extension->load([[]], $this->container); + $this->container->removeDefinition('web_profiler.controller.exception'); $this->assertFalse($this->container->has('web_profiler.debug_toolbar')); - $this->assertSaneContainer($this->getCompiledContainer()); + self::assertSaneContainer($this->getCompiledContainer()); + } + + public function getDebugModes() + { + return [ + ['debug' => false], + ['debug' => true], + ]; } /** - * @dataProvider getDebugModes + * @dataProvider getToolbarConfig */ - public function testToolbarConfig($toolbarEnabled, $interceptRedirects, $listenerInjected, $listenerEnabled) + public function testToolbarConfig(bool $toolbarEnabled, bool $listenerInjected, bool $listenerEnabled) { $extension = new WebProfilerExtension(); - $extension->load([['toolbar' => $toolbarEnabled, 'intercept_redirects' => $interceptRedirects]], $this->container); + $extension->load([['toolbar' => $toolbarEnabled]], $this->container); + $this->container->removeDefinition('web_profiler.controller.exception'); $this->assertSame($listenerInjected, $this->container->has('web_profiler.debug_toolbar')); - $this->assertSaneContainer($this->getCompiledContainer(), '', ['web_profiler.csp.handler']); + self::assertSaneContainer($this->getCompiledContainer(), '', ['web_profiler.csp.handler']); if ($listenerInjected) { $this->assertSame($listenerEnabled, $this->container->get('web_profiler.debug_toolbar')->isEnabled()); } } - public function getDebugModes() + public function getToolbarConfig() + { + return [ + [ + 'toolbarEnabled' => false, + 'listenerInjected' => false, + 'listenerEnabled' => false, + ], + [ + 'toolbarEnabled' => true, + 'listenerInjected' => true, + 'listenerEnabled' => true, + ], + ]; + } + + /** + * @dataProvider getInterceptRedirectsToolbarConfig + */ + public function testToolbarConfigUsingInterceptRedirects( + bool $toolbarEnabled, + bool $interceptRedirects, + bool $listenerInjected, + bool $listenerEnabled + ) { + $extension = new WebProfilerExtension(); + $extension->load( + [['toolbar' => $toolbarEnabled, 'intercept_redirects' => $interceptRedirects]], + $this->container + ); + $this->container->removeDefinition('web_profiler.controller.exception'); + + $this->assertSame($listenerInjected, $this->container->has('web_profiler.debug_toolbar')); + + self::assertSaneContainer($this->getCompiledContainer(), '', ['web_profiler.csp.handler']); + + if ($listenerInjected) { + $this->assertSame($listenerEnabled, $this->container->get('web_profiler.debug_toolbar')->isEnabled()); + } + } + + public function getInterceptRedirectsToolbarConfig() { return [ - [false, false, false, false], - [true, false, true, true], - [false, true, true, false], - [true, true, true, true], + [ + 'toolbarEnabled' => false, + 'interceptRedirects' => true, + 'listenerInjected' => true, + 'listenerEnabled' => false, + ], + [ + 'toolbarEnabled' => false, + 'interceptRedirects' => false, + 'listenerInjected' => false, + 'listenerEnabled' => false, + ], + [ + 'toolbarEnabled' => true, + 'interceptRedirects' => true, + 'listenerInjected' => true, + 'listenerEnabled' => true, + ], ]; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php index 416f63916f042..60c430f9b006f 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/EventListener/WebDebugToolbarListenerTest.php @@ -13,12 +13,13 @@ use PHPUnit\Framework\TestCase; use Symfony\Bundle\WebProfilerBundle\EventListener\WebDebugToolbarListener; -use Symfony\Component\HttpFoundation\HeaderBag; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Event\ResponseEvent; use Symfony\Component\HttpKernel\HttpKernelInterface; +use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; +use Twig\Environment; class WebDebugToolbarListenerTest extends TestCase { @@ -58,11 +59,11 @@ public function getInjectToolbarTests() /** * @dataProvider provideRedirects */ - public function testHtmlRedirectionIsIntercepted($statusCode, $hasSession) + public function testHtmlRedirectionIsIntercepted($statusCode) { $response = new Response('Some content', $statusCode); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(false, 'html', $hasSession), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock('Redirection'), true); $listener->onKernelResponse($event); @@ -75,7 +76,7 @@ public function testNonHtmlRedirectionIsNotIntercepted() { $response = new Response('Some content', '301'); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(false, 'json', true), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock('Redirection'), true); $listener->onKernelResponse($event); @@ -89,7 +90,7 @@ public function testToolbarIsInjected() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -105,7 +106,7 @@ public function testToolbarIsNotInjectedOnNonHtmlContentType() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Type', 'text/xml'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -121,7 +122,7 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); $response->headers->set('Content-Disposition', 'attachment; filename=test.html'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(false, 'html'), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -133,11 +134,11 @@ public function testToolbarIsNotInjectedOnContentDispositionAttachment() * @depends testToolbarIsInjected * @dataProvider provideRedirects */ - public function testToolbarIsNotInjectedOnRedirection($statusCode, $hasSession) + public function testToolbarIsNotInjectedOnRedirection($statusCode) { $response = new Response('', $statusCode); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(false, 'html', $hasSession), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -148,10 +149,8 @@ public function testToolbarIsNotInjectedOnRedirection($statusCode, $hasSession) public function provideRedirects() { return [ - [301, true], - [302, true], - [301, false], - [302, false], + [301], + [302], ]; } @@ -162,7 +161,7 @@ public function testToolbarIsNotInjectedWhenThereIsNoNoXDebugTokenResponseHeader { $response = new Response(''); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -178,7 +177,7 @@ public function testToolbarIsNotInjectedWhenOnSubRequest() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::SUB_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::SUB_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -194,7 +193,7 @@ public function testToolbarIsNotInjectedOnIncompleteHtmlResponses() $response = new Response('
    Some content
    '); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -210,7 +209,10 @@ public function testToolbarIsNotInjectedOnXmlHttpRequests() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(true), HttpKernelInterface::MASTER_REQUEST, $response); + $request = new Request(); + $request->headers->set('X-Requested-With', 'XMLHttpRequest'); + + $event = new ResponseEvent($this->createMock(Kernel::class), $request, HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -226,7 +228,7 @@ public function testToolbarIsNotInjectedOnNonHtmlRequests() $response = new Response(''); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(false, 'json'), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request([], [], ['_format' => 'json']), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock()); $listener->onKernelResponse($event); @@ -239,7 +241,7 @@ public function testXDebugUrlHeader() $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $urlGenerator = $this->getUrlGeneratorMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); $urlGenerator ->expects($this->once()) ->method('generate') @@ -247,7 +249,7 @@ public function testXDebugUrlHeader() ->willReturn('http://mydomain.com/_profiler/xxxxxxxx') ; - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); $listener->onKernelResponse($event); @@ -260,7 +262,7 @@ public function testThrowingUrlGenerator() $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $urlGenerator = $this->getUrlGeneratorMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); $urlGenerator ->expects($this->once()) ->method('generate') @@ -268,7 +270,7 @@ public function testThrowingUrlGenerator() ->willThrowException(new \Exception('foo')) ; - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); $listener->onKernelResponse($event); @@ -281,7 +283,7 @@ public function testThrowingErrorCleanup() $response = new Response(); $response->headers->set('X-Debug-Token', 'xxxxxxxx'); - $urlGenerator = $this->getUrlGeneratorMock(); + $urlGenerator = $this->createMock(UrlGeneratorInterface::class); $urlGenerator ->expects($this->once()) ->method('generate') @@ -289,7 +291,7 @@ public function testThrowingErrorCleanup() ->willThrowException(new \Exception("This\nmultiline\r\ntabbed text should\tcome out\r on\n \ta single plain\r\nline")) ; - $event = new ResponseEvent($this->getKernelMock(), $this->getRequestMock(), HttpKernelInterface::MASTER_REQUEST, $response); + $event = new ResponseEvent($this->createMock(Kernel::class), new Request(), HttpKernelInterface::MASTER_REQUEST, $response); $listener = new WebDebugToolbarListener($this->getTwigMock(), false, WebDebugToolbarListener::ENABLED, $urlGenerator); $listener->onKernelResponse($event); @@ -297,45 +299,13 @@ public function testThrowingErrorCleanup() $this->assertEquals('Exception: This multiline tabbed text should come out on a single plain line', $response->headers->get('X-Debug-Error')); } - protected function getRequestMock($isXmlHttpRequest = false, $requestFormat = 'html', $hasSession = true) - { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->setMethods(['getSession', 'isXmlHttpRequest', 'getRequestFormat'])->disableOriginalConstructor()->getMock(); - $request->expects($this->any()) - ->method('isXmlHttpRequest') - ->willReturn($isXmlHttpRequest); - $request->expects($this->any()) - ->method('getRequestFormat') - ->willReturn($requestFormat); - - $request->headers = new HeaderBag(); - - if ($hasSession) { - $session = $this->getMockBuilder('Symfony\Component\HttpFoundation\Session\Session')->disableOriginalConstructor()->getMock(); - $request->expects($this->any()) - ->method('getSession') - ->willReturn($session); - } - - return $request; - } - protected function getTwigMock($render = 'WDT') { - $templating = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); + $templating = $this->createMock(Environment::class); $templating->expects($this->any()) ->method('render') ->willReturn($render); return $templating; } - - protected function getUrlGeneratorMock() - { - return $this->getMockBuilder('Symfony\Component\Routing\Generator\UrlGeneratorInterface')->getMock(); - } - - protected function getKernelMock() - { - return $this->getMockBuilder('Symfony\Component\HttpKernel\Kernel')->disableOriginalConstructor()->getMock(); - } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.php new file mode 100644 index 0000000000000..06e6d5947bd4f --- /dev/null +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Functional/WebProfilerBundleKernel.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\Bundle\WebProfilerBundle\Tests\Functional; + +use Psr\Log\NullLogger; +use Symfony\Bundle\FrameworkBundle\FrameworkBundle; +use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait; +use Symfony\Bundle\TwigBundle\TwigBundle; +use Symfony\Bundle\WebProfilerBundle\WebProfilerBundle; +use Symfony\Component\Config\Loader\LoaderInterface; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Kernel; +use Symfony\Component\Routing\RouteCollectionBuilder; + +class WebProfilerBundleKernel extends Kernel +{ + use MicroKernelTrait; + + public function __construct() + { + parent::__construct('test', false); + } + + public function registerBundles() + { + return [ + new FrameworkBundle(), + new TwigBundle(), + new WebProfilerBundle(), + ]; + } + + protected function configureRoutes(RouteCollectionBuilder $routes) + { + $routes->import(__DIR__.'/../../Resources/config/routing/profiler.xml', '/_profiler'); + $routes->import(__DIR__.'/../../Resources/config/routing/wdt.xml', '/_wdt'); + $routes->add('/', 'kernel:homepageController'); + } + + protected function configureContainer(ContainerBuilder $containerBuilder, LoaderInterface $loader) + { + $containerBuilder->loadFromExtension('framework', [ + 'secret' => 'foo-secret', + 'profiler' => ['only_exceptions' => false], + 'session' => ['storage_id' => 'session.storage.mock_file'], + ]); + + $containerBuilder->loadFromExtension('web_profiler', [ + 'toolbar' => true, + 'intercept_redirects' => false, + ]); + + $containerBuilder->loadFromExtension('twig', [ + 'strict_variables' => true, + 'exception_controller' => null, + ]); + } + + public function getCacheDir() + { + return sys_get_temp_dir().'/cache-'.spl_object_hash($this); + } + + public function getLogDir() + { + return sys_get_temp_dir().'/log-'.spl_object_hash($this); + } + + protected function build(ContainerBuilder $container) + { + $container->register('logger', NullLogger::class); + } + + public function homepageController() + { + return new Response('Homepage Controller.'); + } +} diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php index 33142dbf06659..5e746c63bffe3 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Profiler/TemplateManagerTest.php @@ -13,12 +13,14 @@ use Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager; use Symfony\Bundle\WebProfilerBundle\Tests\TestCase; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\HttpKernel\Profiler\Profile; +use Symfony\Component\HttpKernel\Profiler\Profiler; use Twig\Environment; +use Twig\Loader\LoaderInterface; +use Twig\Loader\SourceContextLoaderInterface; /** - * Test for TemplateManager class. - * * @author Artur WielogΓ³rski */ class TemplateManagerTest extends TestCase @@ -29,20 +31,20 @@ class TemplateManagerTest extends TestCase protected $twigEnvironment; /** - * @var \Symfony\Component\HttpKernel\Profiler\Profiler + * @var Profiler */ protected $profiler; /** - * @var \Symfony\Bundle\WebProfilerBundle\Profiler\TemplateManager + * @var TemplateManager */ protected $templateManager; - protected function setUp() + protected function setUp(): void { parent::setUp(); - $profiler = $this->mockProfiler(); + $this->profiler = $this->createMock(Profiler::class); $twigEnvironment = $this->mockTwigEnvironment(); $templates = [ 'data_collector.foo' => ['foo', 'FooBundle:Collector:foo'], @@ -50,14 +52,12 @@ protected function setUp() 'data_collector.baz' => ['baz', 'FooBundle:Collector:baz'], ]; - $this->templateManager = new TemplateManager($profiler, $twigEnvironment, $templates); + $this->templateManager = new TemplateManager($this->profiler, $twigEnvironment, $templates); } - /** - * @expectedException \Symfony\Component\HttpKernel\Exception\NotFoundHttpException - */ public function testGetNameOfInvalidTemplate() { + $this->expectException(NotFoundHttpException::class); $this->templateManager->getName(new Profile('token'), 'notexistingpanel'); } @@ -96,37 +96,24 @@ public function profileHasCollectorCallback($panel) } } - protected function mockProfile() - { - return $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profile')->disableOriginalConstructor()->getMock(); - } - protected function mockTwigEnvironment() { - $this->twigEnvironment = $this->getMockBuilder('Twig\Environment')->disableOriginalConstructor()->getMock(); - - $this->twigEnvironment->expects($this->any()) - ->method('loadTemplate') - ->willReturn('loadedTemplate'); - - if (interface_exists('Twig\Loader\SourceContextLoaderInterface')) { - $loader = $this->getMockBuilder('Twig\Loader\SourceContextLoaderInterface')->getMock(); + $this->twigEnvironment = $this->createMock(Environment::class); + + if (Environment::MAJOR_VERSION > 1) { + $loader = $this->createMock(LoaderInterface::class); + $loader + ->expects($this->any()) + ->method('exists') + ->willReturn(true); } else { - $loader = $this->getMockBuilder('Twig\Loader\LoaderInterface')->getMock(); + $loader = $this->createMock(SourceContextLoaderInterface::class); } + $this->twigEnvironment->expects($this->any())->method('getLoader')->willReturn($loader); return $this->twigEnvironment; } - - protected function mockProfiler() - { - $this->profiler = $this->getMockBuilder('Symfony\Component\HttpKernel\Profiler\Profiler') - ->disableOriginalConstructor() - ->getMock(); - - return $this->profiler; - } } class ProfileDummy extends Profile @@ -136,7 +123,7 @@ public function __construct() parent::__construct('token'); } - public function hasCollector($name) + public function hasCollector($name): bool { switch ($name) { case 'foo': diff --git a/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php index c3d691d4d3dbf..a690721ebc018 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Tests/Resources/IconTest.php @@ -20,7 +20,7 @@ class IconTest extends TestCase */ public function testIconFileContents($iconFilePath) { - $this->assertRegExp('~.*~s', file_get_contents($iconFilePath), sprintf('The SVG metadata of the %s icon is different than expected (use the same as the other icons).', $iconFilePath)); + $this->assertMatchesRegularExpression('~.*~s', file_get_contents($iconFilePath), sprintf('The SVG metadata of the %s icon is different than expected (use the same as the other icons).', $iconFilePath)); } public function provideIconFilePaths() diff --git a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php index 44947836335e8..deace08e09fa4 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php +++ b/src/Symfony/Bundle/WebProfilerBundle/Twig/WebProfilerExtension.php @@ -22,6 +22,8 @@ * Twig extension for the profiler. * * @author Fabien Potencier + * + * @internal since Symfony 4.4 */ class WebProfilerExtension extends ProfilerExtension { @@ -42,24 +44,32 @@ class WebProfilerExtension extends ProfilerExtension public function __construct(HtmlDumper $dumper = null) { - $this->dumper = $dumper ?: new HtmlDumper(); - $this->dumper->setOutput($this->output = fopen('php://memory', 'r+b')); + $this->dumper = $dumper ?? new HtmlDumper(); + $this->dumper->setOutput($this->output = fopen('php://memory', 'r+')); } + /** + * @return void + */ public function enter(Profile $profile) { ++$this->stackLevel; } + /** + * @return void + */ public function leave(Profile $profile) { if (0 === --$this->stackLevel) { - $this->dumper->setOutput($this->output = fopen('php://memory', 'r+b')); + $this->dumper->setOutput($this->output = fopen('php://memory', 'r+')); } } /** * {@inheritdoc} + * + * @return TwigFunction[] */ public function getFunctions() { @@ -88,7 +98,7 @@ public function dumpLog(Environment $env, $message, Data $context = null) $message = twig_escape_filter($env, $message); $message = preg_replace('/"(.*?)"/', '"$1"', $message); - if (null === $context || false === strpos($message, '{')) { + if (null === $context || !str_contains($message, '{')) { return ''.$message.''; } diff --git a/src/Symfony/Bundle/WebProfilerBundle/WebProfilerBundle.php b/src/Symfony/Bundle/WebProfilerBundle/WebProfilerBundle.php index 897c3ffb7ff85..7fcc4ec47f780 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/WebProfilerBundle.php +++ b/src/Symfony/Bundle/WebProfilerBundle/WebProfilerBundle.php @@ -21,7 +21,7 @@ class WebProfilerBundle extends Bundle public function boot() { if ('prod' === $this->container->getParameter('kernel.environment')) { - @trigger_error('Using WebProfilerBundle in production is not supported and puts your project at risk, disable it.', E_USER_WARNING); + @trigger_error('Using WebProfilerBundle in production is not supported and puts your project at risk, disable it.', \E_USER_WARNING); } } } diff --git a/src/Symfony/Bundle/WebProfilerBundle/composer.json b/src/Symfony/Bundle/WebProfilerBundle/composer.json index 190fc6d117a92..a847a4e322e62 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/composer.json +++ b/src/Symfony/Bundle/WebProfilerBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/web-profiler-bundle", "type": "symfony-bundle", - "description": "Symfony WebProfilerBundle", + "description": "Provides a development tool that gives detailed information about the execution of any request", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,24 +16,25 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/config": "^4.2|^5.0", + "symfony/framework-bundle": "^4.4|^5.0", "symfony/http-kernel": "^4.4", - "symfony/routing": "^3.4|^4.0|^5.0", + "symfony/polyfill-php80": "^1.16", + "symfony/routing": "^4.3|^5.0", "symfony/twig-bundle": "^4.2|^5.0", - "symfony/var-dumper": "^3.4|^4.0|^5.0", - "twig/twig": "^1.41|^2.10" + "twig/twig": "^1.43|^2.13|^3.0.4" }, "require-dev": { - "symfony/console": "^3.4|^4.0|^5.0", + "symfony/browser-kit": "^4.3|^5.0", + "symfony/console": "^4.3|^5.0", + "symfony/css-selector": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", "symfony/stopwatch": "^3.4|^4.0|^5.0" }, "conflict": { - "symfony/dependency-injection": "<3.4", - "symfony/messenger": "<4.2", - "symfony/var-dumper": "<3.4", - "symfony/form": "<4.3" + "symfony/form": "<4.3", + "symfony/messenger": "<4.2" }, "autoload": { "psr-4": { "Symfony\\Bundle\\WebProfilerBundle\\": "" }, @@ -41,10 +42,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Bundle/WebServerBundle/.gitattributes b/src/Symfony/Bundle/WebServerBundle/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Bundle/WebServerBundle/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md index c056f597a49a2..054ca3e8a7305 100644 --- a/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md +++ b/src/Symfony/Bundle/WebServerBundle/CHANGELOG.md @@ -14,7 +14,7 @@ CHANGELOG 3.4.0 ----- - * WebServer can now use '*' as a wildcard to bind to 0.0.0.0 (INADDR_ANY) + * WebServer can now use `*` as a wildcard to bind to 0.0.0.0 (INADDR_ANY) 3.3.0 ----- diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php index e8d51108ccab5..248b4ca0b7348 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerLogCommand.php @@ -26,11 +26,11 @@ /** * @author GrΓ©goire Pineau * - * @deprecated since Symfony 4.4, to be removed in 5.0; the new Symfony local server has more features, you can use it instead. + * @deprecated since Symfony 4.4, to be removed in 5.0; use ServerLogCommand from symfony/monolog-bridge instead */ class ServerLogCommand extends Command { - private static $bgColor = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; + private const BG_COLOR = ['black', 'blue', 'cyan', 'green', 'magenta', 'red', 'white', 'yellow']; private $el; private $handler; @@ -62,17 +62,16 @@ protected function configure() ->addOption('format', null, InputOption::VALUE_REQUIRED, 'The line format', ConsoleFormatter::SIMPLE_FORMAT) ->addOption('date-format', null, InputOption::VALUE_REQUIRED, 'The date format', ConsoleFormatter::SIMPLE_DATE) ->addOption('filter', null, InputOption::VALUE_REQUIRED, 'An expression to filter log. Example: "level > 200 or channel in [\'app\', \'doctrine\']"') - ->setDescription('Starts a log server that displays logs in real time') + ->setDescription('Start a log server that displays logs in real time') ->setHelp(<<<'EOF' %command.name% starts a log server to display in real time the log messages generated by your application: php %command.full_name% -To get the information as a machine readable format, use the ---filter option: +To filter the log messages using any ExpressionLanguage compatible expression, use the --filter option: -php %command.full_name% --filter=port +php %command.full_name% --filter="level > 200 or channel in ['app', 'doctrine']" EOF ) ; @@ -80,7 +79,7 @@ protected function configure() protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. Use the DebugBundle combined with MonologBridge instead.', \E_USER_DEPRECATED); $filter = $input->getOption('filter'); if ($filter) { @@ -101,12 +100,12 @@ protected function execute(InputInterface $input, OutputInterface $output) 'multiline' => OutputInterface::VERBOSITY_DEBUG <= $output->getVerbosity(), ])); - if (false === strpos($host = $input->getOption('host'), '://')) { + if (!str_contains($host = $input->getOption('host'), '://')) { $host = 'tcp://'.$host; } if (!$socket = stream_socket_server($host, $errno, $errstr)) { - throw new RuntimeException(sprintf('Server start failed on "%s": %s %s.', $host, $errstr, $errno)); + throw new RuntimeException(sprintf('Server start failed on "%s": ', $host).$errstr.' '.$errno); } foreach ($this->getLogs($socket) as $clientId => $message) { @@ -121,11 +120,13 @@ protected function execute(InputInterface $input, OutputInterface $output) continue; } - $this->displayLog($input, $output, $clientId, $record); + $this->displayLog($output, $clientId, $record); } + + return 0; } - private function getLogs($socket) + private function getLogs($socket): iterable { $sockets = [(int) $socket => $socket]; $write = []; @@ -148,12 +149,12 @@ private function getLogs($socket) } } - private function displayLog(InputInterface $input, OutputInterface $output, $clientId, array $record) + private function displayLog(OutputInterface $output, int $clientId, array $record) { if (isset($record['log_id'])) { $clientId = unpack('H*', $record['log_id'])[1]; } - $logBlock = sprintf(' ', self::$bgColor[$clientId % 8]); + $logBlock = sprintf(' ', self::BG_COLOR[$clientId % 8]); $output->write($logBlock); $this->handler->handle($record); diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php index a33f88829f9bb..d52ad02934bde 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerRunCommand.php @@ -57,7 +57,7 @@ protected function configure() new InputOption('docroot', 'd', InputOption::VALUE_REQUIRED, 'Document root, usually where your front controllers are stored'), new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), ]) - ->setDescription('Runs a local web server') + ->setDescription('Run a local web server') ->setHelp(<<<'EOF' %command.name% runs a local web server: By default, the server listens on 127.0.0.1 address and the port number is automatically selected @@ -81,7 +81,7 @@ protected function configure() %command.full_name% --router=app/config/router.php -See also: http://www.php.net/manual/en/features.commandline.webserver.php +See also: https://php.net/features.commandline.webserver EOF ) ; @@ -92,7 +92,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', \E_USER_DEPRECATED); $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); @@ -143,7 +143,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $message = sprintf('Server listening on all interfaces, port %s -- see http://%s', $config->getPort(), $displayAddress); } $io->success($message); - if (ini_get('xdebug.profiler_enable_trigger')) { + if (\ini_get('xdebug.profiler_enable_trigger')) { $io->comment('Xdebug profiler trigger enabled.'); } $io->comment('Quit the server with CONTROL-C.'); diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php index 44008f443890e..bd45408f63de6 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStartCommand.php @@ -58,7 +58,7 @@ protected function configure() new InputOption('router', 'r', InputOption::VALUE_REQUIRED, 'Path to custom router script'), new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), ]) - ->setDescription('Starts a local web server in the background') + ->setDescription('Start a local web server in the background') ->setHelp(<<<'EOF' %command.name% runs a local web server: By default, the server listens on 127.0.0.1 address and the port number is automatically selected @@ -81,7 +81,7 @@ protected function configure() php %command.full_name% --router=app/config/router.php -See also: http://www.php.net/manual/en/features.commandline.webserver.php +See also: https://php.net/features.commandline.webserver EOF ) ; @@ -92,7 +92,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', \E_USER_DEPRECATED); $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); @@ -154,7 +154,7 @@ protected function execute(InputInterface $input, OutputInterface $output) $message = sprintf('Server listening on all interfaces, port %s -- see http://%s', $config->getPort(), $displayAddress); } $io->success($message); - if (ini_get('xdebug.profiler_enable_trigger')) { + if (\ini_get('xdebug.profiler_enable_trigger')) { $io->comment('Xdebug profiler trigger enabled.'); } } @@ -163,5 +163,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } + + return 0; } } diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php index e49a6c5c10fa2..9cf9501a9b9e2 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStatusCommand.php @@ -51,7 +51,7 @@ protected function configure() new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), new InputOption('filter', null, InputOption::VALUE_REQUIRED, 'The value to display (one of port, host, or address)'), ]) - ->setDescription('Outputs the status of the local web server') + ->setDescription('Output the status of the local web server') ->setHelp(<<<'EOF' %command.name% shows the details of the given local web server, such as the address and port where it is listening to: @@ -74,13 +74,13 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', \E_USER_DEPRECATED); $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); $server = new WebServer($this->pidFileDirectory); if ($filter = $input->getOption('filter')) { if ($server->isRunning($input->getOption('pidfile'))) { - list($host, $port) = explode(':', $address = $server->getAddress($input->getOption('pidfile'))); + [$host, $port] = explode(':', $address = $server->getAddress($input->getOption('pidfile'))); if ('address' === $filter) { $output->write($address); } elseif ('host' === $filter) { @@ -102,5 +102,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } } + + return 0; } } diff --git a/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php index fe3a6b65f7645..81c1fdf06d11e 100644 --- a/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php +++ b/src/Symfony/Bundle/WebServerBundle/Command/ServerStopCommand.php @@ -48,7 +48,7 @@ protected function configure() ->setDefinition([ new InputOption('pidfile', null, InputOption::VALUE_REQUIRED, 'PID file'), ]) - ->setDescription('Stops the local web server that was started with the server:start command') + ->setDescription('Stop the local web server that was started with the server:start command') ->setHelp(<<<'EOF' %command.name% stops the local web server: @@ -63,7 +63,7 @@ protected function configure() */ protected function execute(InputInterface $input, OutputInterface $output) { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', \E_USER_DEPRECATED); $io = new SymfonyStyle($input, $output instanceof ConsoleOutputInterface ? $output->getErrorOutput() : $output); @@ -76,5 +76,7 @@ protected function execute(InputInterface $input, OutputInterface $output) return 1; } + + return 0; } } diff --git a/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php index 4b4a245bd0549..63a8197d5e4a0 100644 --- a/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php +++ b/src/Symfony/Bundle/WebServerBundle/DependencyInjection/WebServerExtension.php @@ -42,11 +42,9 @@ public function load(array $configs, ContainerBuilder $container) if (!class_exists(ConsoleFormatter::class)) { $container->removeDefinition('web_server.command.server_log'); } - - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4, the new symfony local server has more feature, you should use it instead.', E_USER_DEPRECATED); } - private function getPublicDirectory(ContainerBuilder $container) + private function getPublicDirectory(ContainerBuilder $container): string { $kernelProjectDir = $container->getParameter('kernel.project_dir'); $publicDir = 'public'; diff --git a/src/Symfony/Bundle/WebServerBundle/LICENSE b/src/Symfony/Bundle/WebServerBundle/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Bundle/WebServerBundle/LICENSE +++ b/src/Symfony/Bundle/WebServerBundle/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Bundle/WebServerBundle/README.md b/src/Symfony/Bundle/WebServerBundle/README.md index 09e514dcb809d..1d733098617d0 100644 --- a/src/Symfony/Bundle/WebServerBundle/README.md +++ b/src/Symfony/Bundle/WebServerBundle/README.md @@ -1,6 +1,9 @@ WebServerBundle =============== +**CAUTION**: this bundle is deprecated since Symfony 4.4. Instead, use the +[Symfony Local Web Server](https://symfony.com/doc/current/setup/symfony_server.html). + WebServerBu 10000 ndle provides commands for running applications using the PHP built-in web server. It simplifies your local development setup because you don't have to configure a proper web server such as Apache or Nginx to run your @@ -9,7 +12,7 @@ application. Resources --------- - * [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) + * [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/src/Symfony/Bundle/WebServerBundle/Resources/router.php b/src/Symfony/Bundle/WebServerBundle/Resources/router.php index 30d6b258a29de..82212df10394c 100644 --- a/src/Symfony/Bundle/WebServerBundle/Resources/router.php +++ b/src/Symfony/Bundle/WebServerBundle/Resources/router.php @@ -12,7 +12,7 @@ /* * This file implements rewrite rules for PHP built-in web server. * - * See: http://www.php.net/manual/en/features.commandline.webserver.php + * See: https://php.net/features.commandline.webserver * * If you have custom directory layout, then you have to write your own router * and pass it as a value to 'router' option of server:run command. @@ -26,18 +26,18 @@ require ini_get('auto_prepend_file'); } -if (is_file($_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { +if (is_file($_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$_SERVER['SCRIPT_NAME'])) { return false; } -$script = isset($_ENV['APP_FRONT_CONTROLLER']) ? $_ENV['APP_FRONT_CONTROLLER'] : 'index.php'; +$script = $_ENV['APP_FRONT_CONTROLLER'] ?? 'index.php'; $_SERVER = array_merge($_SERVER, $_ENV); -$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].DIRECTORY_SEPARATOR.$script; +$_SERVER['SCRIPT_FILENAME'] = $_SERVER['DOCUMENT_ROOT'].\DIRECTORY_SEPARATOR.$script; // Since we are rewriting to app_dev.php, adjust SCRIPT_NAME and PHP_SELF accordingly -$_SERVER['SCRIPT_NAME'] = DIRECTORY_SEPARATOR.$script; -$_SERVER['PHP_SELF'] = DIRECTORY_SEPARATOR.$script; +$_SERVER['SCRIPT_NAME'] = \DIRECTORY_SEPARATOR.$script; +$_SERVER['PHP_SELF'] = \DIRECTORY_SEPARATOR.$script; require $script; diff --git a/src/Symfony/Bundle/WebServerBundle/WebServer.php b/src/Symfony/Bundle/WebServerBundle/WebServer.php index 51efcc0be863f..1cde95ad12f76 100644 --- a/src/Symfony/Bundle/WebServerBundle/WebServer.php +++ b/src/Symfony/Bundle/WebServerBundle/WebServer.php @@ -24,8 +24,8 @@ */ class WebServer { - const STARTED = 0; - const STOPPED = 1; + public const STARTED = 0; + public const STOPPED = 1; private $pidFileDirectory; @@ -149,17 +149,14 @@ public function isRunning($pidFile = null) return false; } - /** - * @return Process The process - */ - private function createServerProcess(WebServerConfig $config) + private function createServerProcess(WebServerConfig $config): Process { $finder = new PhpExecutableFinder(); if (false === $binary = $finder->find(false)) { throw new \RuntimeException('Unable to find the PHP binary.'); } - $xdebugArgs = ini_get('xdebug.profiler_enable_trigger') ? ['-dxdebug.profiler_enable_trigger=1'] : []; + $xdebugArgs = \ini_get('xdebug.profiler_enable_trigger') ? ['-dxdebug.profiler_enable_trigger=1'] : []; $process = new Process(array_merge([$binary], $finder->findArguments(), $xdebugArgs, ['-dvariables_order=EGPCS', '-S', $config->getAddress(), $config->getRouter()])); $process->setWorkingDirectory($config->getDocumentRoot()); @@ -167,13 +164,17 @@ private function createServerProcess(WebServerConfig $config) if (\in_array('APP_ENV', explode(',', getenv('SYMFONY_DOTENV_VARS')))) { $process->setEnv(['APP_ENV' => false]); - $process->inheritEnvironmentVariables(); + + if (!method_exists(Process::class, 'fromShellCommandline')) { + // Symfony 3.4 does not inherit env vars by default: + $process->inheritEnvironmentVariables(); + } } return $process; } - private function getDefaultPidFile() + private function getDefaultPidFile(): string { return ($this->pidFileDirectory ?? getcwd()).'/.web-server-pid'; } diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php index 59cd1c9104323..3b60075a6eff7 100644 --- a/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php +++ b/src/Symfony/Bundle/WebServerBundle/WebServerBundle.php @@ -20,6 +20,6 @@ class WebServerBundle extends Bundle { public function boot() { - @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', E_USER_DEPRECATED); + @trigger_error('Using the WebserverBundle is deprecated since Symfony 4.4. The new Symfony local server has more features, you can use it instead.', \E_USER_DEPRECATED); } } diff --git a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php index a3140bd92e32f..61536573d0c72 100644 --- a/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php +++ b/src/Symfony/Bundle/WebServerBundle/WebServerConfig.php @@ -31,7 +31,7 @@ public function __construct(string $documentRoot, string $env, string $address = } if (null === $file = $this->findFrontController($documentRoot, $env)) { - throw new \InvalidArgumentException(sprintf('Unable to find the front controller under "%s" (none of these files exist: %s).', $documentRoot, implode(', ', $this->getFrontControllerFileNames($env)))); + throw new \InvalidArgumentException(sprintf('Unable to find the front controller under "%s" (none of these files exist: "%s").', $documentRoot, implode('", "', $this->getFrontControllerFileNames($env)))); } $_ENV['APP_FRONT_CONTROLLER'] = $file; @@ -119,7 +119,7 @@ public function getDisplayAddress() return gethostbyname($localHostname).':'.$this->port; } - private function findFrontController($documentRoot, $env) + private function findFrontController(string $documentRoot, string $env): ?string { $fileNames = $this->getFrontControllerFileNames($env); @@ -128,14 +128,16 @@ private function findFrontController($documentRoot, $env) return $fileName; } } + + return null; } - private function getFrontControllerFileNames($env) + private function getFrontControllerFileNames(string $env): array { return ['app_'.$env.'.php', 'app.php', 'index_'.$env.'.php', 'index.php']; } - private function findBestPort() + private function findBestPort(): int { $port = 8000; while (false !== $fp = @fsockopen($this->hostname, $port, $errno, $errstr, 1)) { diff --git a/src/Symfony/Bundle/WebServerBundle/composer.json b/src/Symfony/Bundle/WebServerBundle/composer.json index 8360c3e04ac75..23aad7f95fc9a 100644 --- a/src/Symfony/Bundle/WebServerBundle/composer.json +++ b/src/Symfony/Bundle/WebServerBundle/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/web-server-bundle", "type": "symfony-bundle", - "description": "Symfony WebServerBundle", + "description": "Provides commands for running applications using the PHP built-in web server", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,12 +16,13 @@ } ], "require": { - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/config": "^3.4|^4.0|^5.0", "symfony/console": "^3.4|^4.0|^5.0", "symfony/dependency-injection": "^3.4|^4.0|^5.0", "symfony/http-kernel": "^3.4|^4.0|^5.0", "symfony/polyfill-ctype": "~1.8", + "symfony/polyfill-php80": "^1.16", "symfony/process": "^3.4.2|^4.0.2|^5.0" }, "autoload": { @@ -34,10 +35,5 @@ "symfony/monolog-bridge": "For using the log server.", "symfony/expression-language": "For using the filter option of the log server." }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Component/Asset/.gitattributes b/src/Symfony/Component/Asset/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/Asset/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/Asset/LICENSE b/src/Symfony/Component/Asset/LICENSE index a677f43763ca4..88bf75bb4d6a2 100644 --- a/src/Symfony/Component/Asset/LICENSE +++ b/src/Symfony/Component/Asset/LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2019 Fabien Potencier +Copyright (c) 2004-2022 Fabien Potencier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/Symfony/Component/Asset/Package.php b/src/Symfony/Component/Asset/Package.php index 77b1c934eb172..8a797644398bc 100644 --- a/src/Symfony/Component/Asset/Package.php +++ b/src/Symfony/Component/Asset/Package.php @@ -29,7 +29,7 @@ class Package implements PackageInterface public function __construct(VersionStrategyInterface $versionStrategy, ContextInterface $context = null) { $this->versionStrategy = $versionStrategy; - $this->context = $context ?: new NullContext(); + $this->context = $context ?? new NullContext(); } /** @@ -68,8 +68,11 @@ protected function getVersionStrategy() return $this->versionStrategy; } + /** + * @return bool + */ protected function isAbsoluteUrl($url) { - return false !== strpos($url, '://') || '//' === substr($url, 0, 2); + return str_contains($url, '://') || '//' === substr($url, 0, 2); } } diff --git a/src/Symfony/Component/Asset/Packages.php b/src/Symfony/Component/Asset/Packages.php index 3e82dcdcd407e..49035914ed517 100644 --- a/src/Symfony/Component/Asset/Packages.php +++ b/src/Symfony/Component/Asset/Packages.php @@ -26,8 +26,7 @@ class Packages private $packages = []; /** - * @param PackageInterface $defaultPackage The default package - * @param PackageInterface[] $packages Additional packages indexed by name + * @param PackageInterface[] $packages Additional packages indexed by name */ public function __construct(PackageInterface $defaultPackage = null, array $packages = []) { @@ -46,8 +45,7 @@ public function setDefaultPackage(PackageInterface $defaultPackage) /** * Adds a package. * - * @param string $name The package name - * @param PackageInterface $package The package + * @param string $name The package name */ public function addPackage($name, PackageInterface $package) { diff --git a/src/Symfony/Component/Asset/PathPackage.php b/src/Symfony/Component/Asset/PathPackage.php index e3cc56c0f7113..19141e57d6d5a 100644 --- a/src/Symfony/Component/Asset/PathPackage.php +++ b/src/Symfony/Component/Asset/PathPackage.php @@ -29,9 +29,7 @@ class PathPackage extends Package private $basePath; /** - * @param string $basePath The base path to be prepended to relative paths - * @param VersionStrategyInterface $versionStrategy The version strategy - * @param ContextInterface|null $context The context + * @param string $basePath The base path to be prepended to relative paths */ public function __construct(string $basePath, VersionStrategyInterface $versionStrategy, ContextInterface $context = null) { diff --git a/src/Symfony/Component/Asset/README.md b/src/Symfony/Component/Asset/README.md index 3291698493bc1..37e7ea37014ff 100644 --- a/src/Symfony/Component/Asset/README.md +++ b/src/Symfony/Component/Asset/README.md @@ -7,8 +7,8 @@ CSS stylesheets, JavaScript files and image files. Resources --------- - * [Documentation](https://symfony.com/doc/current/components/asset/introduction.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) + * [Documentation](https://symfony.com/doc/current/components/asset/introduction.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/src/Symfony/Component/Asset/Tests/Context/RequestStackContextTest.php b/src/Symfony/Component/Asset/Tests/Context/RequestStackContextTest.php index 7f24534eba202..ed323749af046 100644 --- a/src/Symfony/Component/Asset/Tests/Context/RequestStackContextTest.php +++ b/src/Symfony/Component/Asset/Tests/Context/RequestStackContextTest.php @@ -13,12 +13,14 @@ use PHPUnit\Framework\TestCase; use Symfony\Component\Asset\Context\RequestStackContext; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\RequestStack; class RequestStackContextTest extends TestCase { public function testGetBasePathEmpty() { - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStackContext = new RequestStackContext($requestStack); $this->assertEmpty($requestStackContext->getBasePath()); @@ -28,21 +30,21 @@ public function testGetBasePathSet() { $testBasePath = 'test-path'; - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); + $request = $this->createMock(Request::class); $request->method('getBasePath') ->willReturn($testBasePath); - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStack->method('getMasterRequest') ->willReturn($request); $requestStackContext = new RequestStackContext($requestStack); - $this->assertEquals($testBasePath, $requestStackContext->getBasePath()); + $this->assertSame($testBasePath, $requestStackContext->getBasePath()); } public function testIsSecureFalse() { - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStackContext = new RequestStackContext($requestStack); $this->assertFalse($requestStackContext->isSecure()); @@ -50,10 +52,10 @@ public function testIsSecureFalse() public function testIsSecureTrue() { - $request = $this->getMockBuilder('Symfony\Component\HttpFoundation\Request')->getMock(); + $request = $this->createMock(Request::class); $request->method('isSecure') ->willReturn(true); - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStack->method('getMasterRequest') ->willReturn($request); @@ -64,7 +66,7 @@ public function testIsSecureTrue() public function testDefaultContext() { - $requestStack = $this->getMockBuilder('Symfony\Component\HttpFoundation\RequestStack')->getMock(); + $requestStack = $this->createMock(RequestStack::class); $requestStackContext = new RequestStackContext($requestStack, 'default-path', true); $this->assertSame('default-path', $requestStackContext->getBasePath()); diff --git a/src/Symfony/Component/Asset/Tests/PackageTest.php b/src/Symfony/Component/Asset/Tests/PackageTest.php index 8f6626ae4d2ad..8d7f7b8a26a71 100644 --- a/src/Symfony/Component/Asset/Tests/PackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PackageTest.php @@ -24,7 +24,7 @@ class PackageTest extends TestCase public function testGetUrl($version, $format, $path, $expected) { $package = new Package($version ? new StaticVersionStrategy($version, $format) : new EmptyVersionStrategy()); - $this->assertEquals($expected, $package->getUrl($path)); + $this->assertSame($expected, $package->getUrl($path)); } public function getConfigs() @@ -50,6 +50,6 @@ public function getConfigs() public function testGetVersion() { $package = new Package(new StaticVersionStrategy('v1')); - $this->assertEquals('v1', $package->getVersion('/foo')); + $this->assertSame('v1', $package->getVersion('/foo')); } } diff --git a/src/Symfony/Component/Asset/Tests/PackagesTest.php b/src/Symfony/Component/Asset/Tests/PackagesTest.php index b751986d48dd0..38044a93654eb 100644 --- a/src/Symfony/Component/Asset/Tests/PackagesTest.php +++ b/src/Symfony/Component/Asset/Tests/PackagesTest.php @@ -12,7 +12,10 @@ namespace Symfony\Component\Asset\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; use Symfony\Component\Asset\Package; +use Symfony\Component\Asset\PackageInterface; use Symfony\Component\Asset\Packages; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; @@ -21,16 +24,16 @@ class PackagesTest extends TestCase public function testGetterSetters() { $packages = new Packages(); - $packages->setDefaultPackage($default = $this->getMockBuilder('Symfony\Component\Asset\PackageInterface')->getMock()); - $packages->addPackage('a', $a = $this->getMockBuilder('Symfony\Component\Asset\PackageInterface')->getMock()); + $packages->setDefaultPackage($default = $this->createMock(PackageInterface::class)); + $packages->addPackage('a', $a = $this->createMock(PackageInterface::class)); - $this->assertEquals($default, $packages->getPackage()); - $this->assertEquals($a, $packages->getPackage('a')); + $this->assertSame($default, $packages->getPackage()); + $this->assertSame($a, $packages->getPackage('a')); $packages = new Packages($default, ['a' => $a]); - $this->assertEquals($default, $packages->getPackage()); - $this->assertEquals($a, $packages->getPackage('a')); + $this->assertSame($default, $packages->getPackage()); + $this->assertSame($a, $packages->getPackage('a')); } public function testGetVersion() @@ -40,8 +43,8 @@ public function testGetVersion() ['a' => new Package(new StaticVersionStrategy('a'))] ); - $this->assertEquals('default', $packages->getVersion('/foo')); - $this->assertEquals('a', $packages->getVersion('/foo', 'a')); + $this->assertSame('default', $packages->getVersion('/foo')); + $this->assertSame('a', $packages->getVersion('/foo', 'a')); } public function testGetUrl() @@ -51,24 +54,20 @@ public function testGetUrl() ['a' => new Package(new StaticVersionStrategy('a'))] ); - $this->assertEquals('/foo?default', $packages->getUrl('/foo')); - $this->assertEquals('/foo?a', $packages->getUrl('/foo', 'a')); + $this->assertSame('/foo?default', $packages->getUrl('/foo')); + $this->assertSame('/foo?a', $packages->getUrl('/foo', 'a')); } - /** - * @expectedException \Symfony\Component\Asset\Exception\LogicException - */ public function testNoDefaultPackage() { + $this->expectException(LogicException::class); $packages = new Packages(); $packages->getPackage(); } - /** - * @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException - */ public function testUndefinedPackage() { + $this->expectException(InvalidArgumentException::class); $packages = new Packages(); $packages->getPackage('a'); } diff --git a/src/Symfony/Component/Asset/Tests/PathPackageTest.php b/src/Symfony/Component/Asset/Tests/PathPackageTest.php index d00cc76c2a943..57bb80bcccf06 100644 --- a/src/Symfony/Component/Asset/Tests/PathPackageTest.php +++ b/src/Symfony/Component/Asset/Tests/PathPackageTest.php @@ -12,8 +12,10 @@ namespace Symfony\Component\Asset\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Asset\Context\ContextInterface; use Symfony\Component\Asset\PathPackage; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; class PathPackageTest extends TestCase { @@ -23,7 +25,7 @@ class PathPackageTest extends TestCase public function testGetUrl($basePath, $format, $path, $expected) { $package = new PathPackage($basePath, new StaticVersionStrategy('v1', $format)); - $this->assertEquals($expected, $package->getUrl($path)); + $this->assertSame($expected, $package->getUrl($path)); } public function getConfigs() @@ -55,7 +57,7 @@ public function testGetUrlWithContext($basePathRequest, $basePath, $format, $pat { $package = new PathPackage($basePath, new StaticVersionStrategy('v1', $format), $this->getContext($basePathRequest)); - $this->assertEquals($expected, $package->getUrl($path)); + $this->assertSame($expected, $package->getUrl($path)); } public function getContextConfigs() @@ -77,18 +79,18 @@ public function getContextConfigs() public function testVersionStrategyGivesAbsoluteURL() { - $versionStrategy = $this->getMockBuilder('Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface')->getMock(); + $versionStrategy = $this->createMock(VersionStrategyInterface::class); $versionStrategy->expects($this->any()) ->method('applyVersion') ->willReturn('https://cdn.com/bar/main.css'); $package = new PathPackage('/subdirectory', $versionStrategy, $this->getContext('/bar')); - $this->assertEquals('https://cdn.com/bar/main.css', $package->getUrl('main.css')); + $this->assertSame('https://cdn.com/bar/main.css', $package->getUrl('main.css')); } private function getContext($basePath) { - $context = $this->getMockBuilder('Symfony\Component\Asset\Context\ContextInterface')->getMock(); + $context = $this->createMock(ContextInterface::class); $context->expects($this->any())->method('getBasePath')->willReturn($basePath); return $context; diff --git a/src/Symfony/Component/Asset/Tests/UrlPackageTest.php b/src/Symfony/Component/Asset/Tests/UrlPackageTest.php index 3bb06633d32a6..717c0687c9875 100644 --- a/src/Symfony/Component/Asset/Tests/UrlPackageTest.php +++ b/src/Symfony/Component/Asset/Tests/UrlPackageTest.php @@ -12,9 +12,13 @@ namespace Symfony\Component\Asset\Tests; use PHPUnit\Framework\TestCase; +use Symfony\Component\Asset\Context\ContextInterface; +use Symfony\Component\Asset\Exception\InvalidArgumentException; +use Symfony\Component\Asset\Exception\LogicException; use Symfony\Component\Asset\UrlPackage; use Symfony\Component\Asset\VersionStrategy\EmptyVersionStrategy; use Symfony\Component\Asset\VersionStrategy\StaticVersionStrategy; +use Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface; class UrlPackageTest extends TestCase { @@ -24,7 +28,7 @@ class UrlPackageTest extends TestCase public function testGetUrl($baseUrls, $format, $path, $expected) { $package = new UrlPackage($baseUrls, new StaticVersionStrategy('v1', $format)); - $this->assertEquals($expected, $package->getUrl($path)); + $this->assertSame($expected, $package->getUrl($path)); } public function getConfigs() @@ -65,7 +69,7 @@ public function testGetUrlWithContext($secure, $baseUrls, $format, $path, $expec { $package = new UrlPackage($baseUrls, new StaticVersionStrategy('v1', $format), $this->getContext($secure)); - $this->assertEquals($expected, $package->getUrl($path)); + $this->assertSame($expected, $package->getUrl($path)); } public function getContextConfigs() @@ -86,30 +90,27 @@ public function getContextConfigs() public function testVersionStrategyGivesAbsoluteURL() { - $versionStrategy = $this->getMockBuilder('Symfony\Component\Asset\VersionStrategy\VersionStrategyInterface')->getMock(); + $versionStrategy = $this->createMock(VersionStrategyInterface::class); $versionStrategy->expects($this->any()) ->method('applyVersion') ->willReturn('https://cdn.com/bar/main.css'); $package = new UrlPackage('https://example.com', $versionStrategy); - $this->assertEquals('https://cdn.com/bar/main.css', $package->getUrl('main.css')); + $this->assertSame('https://cdn.com/bar/main.css', $package->getUrl('main.css')); } - /** - * @expectedException \Symfony\Component\Asset\Exception\LogicException - */ public function testNoBaseUrls() { + $this->expectException(LogicException::class); new UrlPackage([], new EmptyVersionStrategy()); } /** * @dataProvider getWrongBaseUrlConfig - * - * @expectedException \Symfony\Component\Asset\Exception\InvalidArgumentException */ public function testWrongBaseUrl($baseUrls) { + $this->expectException(InvalidArgumentException::class); new UrlPackage($baseUrls, new EmptyVersionStrategy()); } @@ -123,7 +124,7 @@ public function getWrongBaseUrlConfig() private function getContext($secure) { - $context = $this->getMockBuilder('Symfony\Component\Asset\Context\ContextInterface')->getMock(); + $context = $this->createMock(ContextInterface::class); $context->expects($this->any())->method('isSecure')->willReturn($secure); return $context; diff --git a/src/Symfony/Component/Asset/Tests/VersionStrategy/EmptyVersionStrategyTest.php b/src/Symfony/Component/Asset/Tests/VersionStrategy/EmptyVersionStrategyTest.php index 430146fd5070b..1728c2e99b4d4 100644 --- a/src/Symfony/Component/Asset/Tests/VersionStrategy/EmptyVersionStrategyTest.php +++ b/src/Symfony/Component/Asset/Tests/VersionStrategy/EmptyVersionStrategyTest.php @@ -29,6 +29,6 @@ public function testApplyVersion() $emptyVersionStrategy = new EmptyVersionStrategy(); $path = 'test-path'; - $this->assertEquals($path, $emptyVersionStrategy->applyVersion($path)); + $this->assertSame($path, $emptyVersionStrategy->applyVersion($path)); } } diff --git a/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php index 9da2b4ada2856..a9ca035fb997e 100644 --- a/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php +++ b/src/Symfony/Component/Asset/Tests/VersionStrategy/JsonManifestVersionStrategyTest.php @@ -20,38 +20,34 @@ public function testGetVersion() { $strategy = $this->createStrategy('manifest-valid.json'); - $this->assertEquals('main.123abc.js', $strategy->getVersion('main.js')); + $this->assertSame('main.123abc.js', $strategy->getVersion('main.js')); } public function testApplyVersion() { $strategy = $this->createStrategy('manifest-valid.json'); - $this->assertEquals('css/styles.555def.css', $strategy->getVersion('css/styles.css')); + $this->assertSame('css/styles.555def.css', $strategy->applyVersion('css/styles.css')); } public function testApplyVersionWhenKeyDoesNotExistInManifest() { $strategy = $this->createStrategy('manifest-valid.json'); - $this->assertEquals('css/other.css', $strategy->getVersion('css/other.css')); + $this->assertSame('css/other.css', $strategy->applyVersion('css/other.css')); } - /** - * @expectedException \RuntimeException - */ public function testMissingManifestFileThrowsException() { + $this->expectException(\RuntimeException::class); $strategy = $this->createStrategy('non-existent-file.json'); $strategy->getVersion('main.js'); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Error parsing JSON - */ public function testManifestFileWithBadJSONThrowsException() { + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Error parsing JSON'); $strategy = $this->createStrategy('manifest-invalid.json'); $strategy->getVersion('main.js'); } diff --git a/src/Symfony/Component/Asset/Tests/VersionStrategy/StaticVersionStrategyTest.php b/src/Symfony/Component/Asset/Tests/VersionStrategy/StaticVersionStrategyTest.php index c56a8726a8459..d054e842f2c55 100644 --- a/src/Symfony/Component/Asset/Tests/VersionStrategy/StaticVersionStrategyTest.php +++ b/src/Symfony/Component/Asset/Tests/VersionStrategy/StaticVersionStrategyTest.php @@ -21,7 +21,7 @@ public function testGetVersion() $version = 'v1'; $path = 'test-path'; $staticVersionStrategy = new StaticVersionStrategy($version); - $this->assertEquals($version, $staticVersionStrategy->getVersion($path)); + $this->assertSame($version, $staticVersionStrategy->getVersion($path)); } /** @@ -31,7 +31,7 @@ public function testApplyVersion($path, $version, $format) { $staticVersionStrategy = new StaticVersionStrategy($version, $format); $formatted = sprintf($format ?: '%s?%s', $path, $version); - $this->assertEquals($formatted, $staticVersionStrategy->applyVersion($path)); + $this->assertSame($formatted, $staticVersionStrategy->applyVersion($path)); } public function getConfigs() diff --git a/src/Symfony/Component/Asset/UrlPackage.php b/src/Symfony/Component/Asset/UrlPackage.php index cb949d5969dba..f68dd54fe639e 100644 --- a/src/Symfony/Component/Asset/UrlPackage.php +++ b/src/Symfony/Component/Asset/UrlPackage.php @@ -39,9 +39,7 @@ class UrlPackage extends Package private $sslPackage; /** - * @param string|string[] $baseUrls Base asset URLs - * @param VersionStrategyInterface $versionStrategy The version strategy - * @param ContextInterface|null $context Context + * @param string|string[] $baseUrls Base asset URLs */ public function __construct($baseUrls, VersionStrategyInterface $versionStrategy, ContextInterface $context = null) { @@ -123,14 +121,14 @@ protected function chooseBaseUrl($path) return (int) fmod(hexdec(substr(hash('sha256', $path), 0, 10)), \count($this->baseUrls)); } - private function getSslUrls($urls) + private function getSslUrls(array $urls) { $sslUrls = []; foreach ($urls as $url) { if ('https://' === substr($url, 0, 8) || '//' === substr($url, 0, 2)) { $sslUrls[] = $url; - } elseif (null === parse_url($url, PHP_URL_SCHEME)) { - throw new InvalidArgumentException(sprintf('"%s" is not a valid URL', $url)); + } elseif (null === parse_url($url, \PHP_URL_SCHEME)) { + throw new InvalidArgumentException(sprintf('"%s" is not a valid URL.', $url)); } } diff --git a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php index 7bbfa90786ef9..998c636e6134a 100644 --- a/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php +++ b/src/Symfony/Component/Asset/VersionStrategy/JsonManifestVersionStrategy.php @@ -50,19 +50,19 @@ public function applyVersion($path) return $this->getManifestPath($path) ?: $path; } - private function getManifestPath($path) + private function getManifestPath(string $path): ?string { if (null === $this->manifestData) { if (!file_exists($this->manifestPath)) { - throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist.', $this->manifestPath)); + throw new \RuntimeException(sprintf('Asset manifest file "%s" does not exist. Did you forget to build the assets with npm or yarn?', $this->manifestPath)); } $this->manifestData = json_decode(file_get_contents($this->manifestPath), true); if (0 < json_last_error()) { - throw new \RuntimeException(sprintf('Error parsing JSON from asset manifest file "%s" - %s', $this->manifestPath, json_last_error_msg())); + throw new \RuntimeException(sprintf('Error parsing JSON from asset manifest file "%s": ', $this->manifestPath).json_last_error_msg()); } } - return isset($this->manifestData[$path]) ? $this->manifestData[$path] : null; + return $this->manifestData[$path] ?? null; } } diff --git a/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php b/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php index e7ce0ec218976..d752cd4c2f13d 100644 --- a/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php +++ b/src/Symfony/Component/Asset/VersionStrategy/StaticVersionStrategy.php @@ -46,7 +46,7 @@ public function applyVersion($path) { $versionized = sprintf($this->format, ltrim($path, '/'), $this->getVersion($path)); - if ($path && '/' == $path[0]) { + if ($path && '/' === $path[0]) { return '/'.$versionized; } diff --git a/src/Symfony/Component/Asset/composer.json b/src/Symfony/Component/Asset/composer.json index fc12a5bdb84c3..c0281a78aa7f3 100644 --- a/src/Symfony/Component/Asset/composer.json +++ b/src/Symfony/Component/Asset/composer.json @@ -1,7 +1,7 @@ { "name": "symfony/asset", "type": "library", - "description": "Symfony Asset Component", + "description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files", "keywords": [], "homepage": "https://symfony.com", "license": "MIT", @@ -16,7 +16,8 @@ } ], "require": { - "php": "^7.1.3" + "php": ">=7.1.3", + "symfony/polyfill-php80": "^1.16" }, "suggest": { "symfony/http-foundation": "" @@ -31,10 +32,5 @@ "/Tests/" ] }, - "minimum-stability": "dev", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - } + "minimum-stability": "dev" } diff --git a/src/Symfony/Component/BrowserKit/.gitattributes b/src/Symfony/Component/BrowserKit/.gitattributes new file mode 100644 index 0000000000000..84c7add058fb5 --- /dev/null +++ b/src/Symfony/Component/BrowserKit/.gitattributes @@ -0,0 +1,4 @@ +/Tests export-ignore +/phpunit.xml.dist export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore diff --git a/src/Symfony/Component/BrowserKit/Client.php b/src/Symfony/Component/BrowserKit/Client.php index f7d0c36b5b05e..e299aa2d855f4 100644 --- a/src/Symfony/Component/BrowserKit/Client.php +++ b/src/Symfony/Component/BrowserKit/Client.php @@ -50,15 +50,13 @@ abstract class Client private $isMainRequest = true; /** - * @param array $server The server parameters (equivalent of $_SERVER) - * @param History $history A History instance to store the browser history - * @param CookieJar $cookieJar A CookieJar instance to store the cookies + * @param array $server The server parameters (equivalent of $_SERVER) */ public function __construct(array $server = [], History $history = null, CookieJar $cookieJar = null) { $this->setServerParameters($server); - $this->history = $history ?: new History(); - $this->cookieJar = $cookieJar ?: new CookieJar(); + $this->history = $history ?? new History(); + $this->cookieJar = $cookieJar ?? new CookieJar(); } /** @@ -119,7 +117,7 @@ public function getMaxRedirects() */ public function insulate($insulated = true) { - if ($insulated && !class_exists('Symfony\\Component\\Process\\Process')) { + if ($insulated && !class_exists(\Symfony\Component\Process\Process::class)) { throw new \LogicException('Unable to isolate requests as the Symfony Process Component is not installed.'); } @@ -153,13 +151,13 @@ public function setServerParameter($key, $value) * Gets single server parameter for specified key. * * @param string $key A key of the parameter to get - * @param string $default A default value when key is undefined + * @param mixed $default A default value when key is undefined * - * @return string A value of the parameter + * @return mixed A value of the parameter */ public function getServerParameter($key, $default = '') { - return isset($this->server[$key]) ? $this->server[$key] : $default; + return $this->server[$key] ?? $default; } public function xmlHttpRequest(string $method, string $uri, array $parameters = [], array $files = [], array $server = [], string $content = null, bool $changeHistory = true): Crawler @@ -201,7 +199,7 @@ public function getCookieJar() public function getCrawler() { if (null === $this->crawler) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -216,7 +214,7 @@ public function getCrawler() public function getInternalResponse() { if (null === $this->internalResponse) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -236,7 +234,7 @@ public function getInternalResponse() public function getResponse() { if (null === $this->response) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -251,7 +249,7 @@ public function getResponse() public function getInternalRequest() { if (null === $this->internalRequest) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -271,7 +269,7 @@ public function getInternalRequest() public function getRequest() { if (null === $this->request) { - @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + @trigger_error(sprintf('Calling the "%s()" method before the "request()" one is deprecated since Symfony 4.1 and will throw an exception in 5.0.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); // throw new BadMethodCallException(sprintf('The "request()" method must be called before "%s()".', __METHOD__)); } @@ -309,16 +307,15 @@ public function clickLink(string $linkText): Crawler /** * Submits a form. * - * @param Form $form A Form instance * @param array $values An array of form field values * @param array $serverParameters An array of server parameters * * @return Crawler */ - public function submit(Form $form, array $values = []/*, array $serverParameters = []*/) + public function submit(Form $form, array $values = []/* , array $serverParameters = [] */) { - if (\func_num_args() < 3 && __CLASS__ !== \get_class($this) && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface) { - @trigger_error(sprintf('The "%s()" method will have a new "array $serverParameters = []" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', \get_class($this).'::'.__FUNCTION__), E_USER_DEPRECATED); + if (\func_num_args() < 3 && __CLASS__ !== static::class && __CLASS__ !== (new \ReflectionMethod($this, __FUNCTION__))->getDeclaringClass()->getName() && !$this instanceof \PHPUnit\Framework\MockObject\MockObject && !$this instanceof \Prophecy\Prophecy\ProphecySubjectInterface && !$this instanceof \Mockery\MockInterface) { + @trigger_error(sprintf('The "%s()" method will have a new "array $serverParameters = []" argument in version 5.0, not defining it is deprecated since Symfony 4.2.', static::class.'::'.__FUNCTION__), \E_USER_DEPRECATED); } $form->setValues($values); @@ -334,7 +331,7 @@ public function submit(Form $form, array $values = []/*, array $serverParameters * @param string $button The text content, id, value or name of the form