diff --git a/.appveyor.yml b/.appveyor.yml index 3cbe5480282b1..66409a9c4e34b 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -51,7 +51,7 @@ install: - php composer.phar global require --no-progress --no-scripts --no-plugins symfony/flex - git config --global user.email "" - git config --global user.name "Symfony" - - FOR /F "tokens=* USEBACKQ" %%F IN (`bash -c "grep -m1 SYMFONY_VERSION .travis.yml | grep -o '[0-9.x]*'"`) DO (SET SYMFONY_VERSION=%%F) + - 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 "SYMFONY_REQUIRE=>=%SYMFONY_VERSION%" - SET COMPOSER_ROOT_VERSION=%SYMFONY_VERSION%.x-dev diff --git a/.github/workflows/tests.yml b/.github/workflows/integration-tests.yml similarity index 71% rename from .github/workflows/tests.yml rename to .github/workflows/integration-tests.yml index 530fdb747c8cb..5b68583a06a14 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/integration-tests.yml @@ -1,4 +1,4 @@ -name: Tests +name: Integration on: push: @@ -9,8 +9,9 @@ defaults: shell: bash jobs: - integration: - name: Integration + + tests: + name: Tests runs-on: Ubuntu-20.04 strategy: @@ -137,30 +138,18 @@ jobs: entrypoint: /bin/bash args: -c "(/opt/bitnami/openldap/bin/ldapwhoami -h localhost:3389 -D cn=admin,dc=symfony,dc=com -w symfony||sleep 5) && /opt/bitnami/openldap/bin/ldapadd -h 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:3389 -D cn=admin,dc=symfony,dc=com -w symfony cn=a,ou=users,dc=symfony,dc=com" - - name: Configure composer + - name: Install dependencies run: | COMPOSER_HOME="$(composer config home)" - composer self-update ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" - echo "COMPOSER_ROOT_VERSION=$(grep -m1 SYMFONY_VERSION .travis.yml | grep -o '[0-9.x]*').x-dev" >> $GITHUB_ENV - - - name: Determine composer cache directory - id: composer-cache - run: echo "::set-output name=directory::$(composer config cache-dir)" - - - name: Cache composer dependencies - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.directory }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + 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 - - name: Install dependencies - run: | echo "::group::composer update" composer require --dev --no-update mongodb/mongodb:@stable composer update --no-progress --ansi echo "::endgroup::" + echo "::group::install phpunit" ./phpunit install echo "::endgroup::" @@ -191,58 +180,3 @@ jobs: # 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 - - nightly: - name: PHPUnit on PHP nightly - runs-on: Ubuntu-20.04 - - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - coverage: "none" - ini-values: "memory_limit=-1" - php-version: "8.1" - - - name: Configure composer - run: | - COMPOSER_HOME="$(composer config home)" - composer self-update - ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" - echo "COMPOSER_ROOT_VERSION=$(grep -m1 SYMFONY_VERSION .travis.yml | grep -o '[0-9.x]*').x-dev" >> $GITHUB_ENV - - - name: Install dependencies - run: | - echo "::group::fake PHP version" - composer config platform.php 8.0.99 - echo "::group::composer update" - composer update --no-progress --ansi - echo "::endgroup::" - echo "::group::install phpunit" - ./phpunit install - echo "::endgroup::" - - - name: Run tests - run: | - _run_tests() { - ok=0 - echo "::group::$1" - - # Run the tests - ./phpunit --colors=always --exclude-group tty,benchmark,intl-data ./$1 2>&1 || ok=1 - echo ::endgroup:: - - if [ $ok -ne 0 ]; then - echo "::error::$1 failed" - fi - - # Make the tests always pass because we don't want the build to fail (yet). - return 0 - #return $ok - } - export -f _run_tests - - find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' -print0 | xargs -0 -n1 dirname | sort | parallel _run_tests diff --git a/.github/workflows/intl-data-tests.yml b/.github/workflows/intl-data-tests.yml index 6a99694cd4f1d..450826f443874 100644 --- a/.github/workflows/intl-data-tests.yml +++ b/.github/workflows/intl-data-tests.yml @@ -1,4 +1,4 @@ -name: Intl data tests +name: Intl data on: push: @@ -14,7 +14,7 @@ defaults: jobs: tests: - name: Tests (intl-data) + name: Tests runs-on: Ubuntu-20.04 steps: @@ -44,29 +44,17 @@ jobs: ini-values: "memory_limit=-1" php-version: "7.4" - - name: Configure composer + - name: Install dependencies run: | COMPOSER_HOME="$(composer config home)" - composer self-update ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" - echo "COMPOSER_ROOT_VERSION=$(grep -m1 SYMFONY_VERSION .travis.yml | grep -o '[0-9.x]*').x-dev" >> $GITHUB_ENV - - - name: Determine composer cache directory - id: composer-cache - run: echo "::set-output name=directory::$(composer config cache-dir)" - - - name: Cache composer dependencies - uses: actions/cache@v1 - with: - path: ${{ steps.composer-cache.outputs.directory }} - key: ${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: ${{ matrix.php }}-composer- + 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 - - name: Install dependencies - run: | echo "::group::composer update" composer update --no-progress --ansi echo "::endgroup::" + echo "::group::install phpunit" ./phpunit install echo "::endgroup::" diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml index 3a2ec919a49ad..a9adb8e7cf532 100644 --- a/.github/workflows/psalm.yml +++ b/.github/workflows/psalm.yml @@ -13,7 +13,7 @@ jobs: runs-on: Ubuntu-20.04 steps: - - name: Set up PHP + - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: '8.0' @@ -29,22 +29,19 @@ jobs: - name: Checkout PR uses: actions/checkout@v2 - - name: Configure composer + - name: Install dependencies run: | COMPOSER_HOME="$(composer config home)" ([ -d "$COMPOSER_HOME" ] || mkdir "$COMPOSER_HOME") && cp .github/composer-config.json "$COMPOSER_HOME/config.json" - echo "COMPOSER_ROOT_VERSION=$(grep -m1 SYMFONY_VERSION .travis.yml | grep -o '[0-9.x]*').x-dev" >> $GITHUB_ENV + 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-update psalm/phar phpunit/phpunit:^9.5 php-http/discovery psr/event-dispatcher mongodb/mongodb - - name: Install Psalm - run: | - echo "::group::modify composer.json" - composer remove --no-update --no-interaction symfony/phpunit-bridge - composer require --no-update psalm/phar phpunit/phpunit:^9.5 php-http/discovery psr/event-dispatcher - echo "::endgroup::" echo "::group::composer update" composer update --no-progress --ansi git checkout composer.json echo "::endgroup::" + ./vendor/bin/psalm.phar --version - name: Generate Psalm baseline diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml new file mode 100644 index 0000000000000..ceffbe310240c --- /dev/null +++ b/.github/workflows/unit-tests.yml @@ -0,0 +1,220 @@ +name: PHPUnit + +on: + push: + pull_request: + +defaults: + run: + shell: bash + +jobs: + + tests: + name: Tests + runs-on: Ubuntu-20.04 + + env: + extensions: amqp,apcu,igbinary,intl,mbstring,memcached,mongodb,redis + + strategy: + matrix: + include: + - php: '7.2' + - php: '8.0' + - php: '7.4' + mode: high-deps + - php: '8.0' + mode: low-deps + - php: '8.1' + mode: experimental + fail-fast: false + + steps: + - name: Checkout + uses: actions/checkout@v2 + with: + fetch-depth: 2 + + - name: Configure for PHP 8.1 + if: "${{ matrix.php == '8.1' }}" + run: | + echo "extensions=mbstring" >> $GITHUB_ENV + composer config platform.php 8.0.99 + composer require --dev --no-update masterminds/html5:~2.7.5@dev + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + ini-values: date.timezone=Europe/Paris,memory_limit=-1,default_socket_timeout=10,session.gc_probability=0,apc.enable_cli=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="$(readlink -f ./phpunit) --exclude-group tty,benchmark,intl-data" >> $GITHUB_ENV + echo COMPOSER_UP='composer update --no-progress --ansi' >> $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 | grep -P -o '[0-9]+\.[0-9]+') + SYMFONY_FEATURE_BRANCH=$(curl -s https://flex.symfony.com/versions.json | jq -r '."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/*' -printf '%h ') >> $GITHUB_ENV + else + echo COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h ') >> $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.0' && ! matrix.mode }}" + run: | + sed -i 's/"\*\*\/Tests\/"//' composer.json + composer install --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 + + (cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; composer require --dev --no-update mongodb/mongodb) + + 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 + + # matrix.mode = high-deps + echo "$COMPONENTS" | xargs -n1 | parallel -j +3 "_run_tests {} 'cd {} && $COMPOSER_UP && $PHPUNIT$LEGACY'" || X=1 + + (cd src/Symfony/Component/HttpFoundation; mv composer.bak composer.json) + COMPONENTS=$(git diff --name-only src/ | grep composer.json || true) + + if [[ $COMPONENTS && $SYMFONY_VERSION = *.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 + COMPONENTS=$(echo "$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 [[ $COMPONENTS ]]; then + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + echo "$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 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/.travis.yml b/.travis.yml index e3296eecc9985..2836521932ddf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,40 +3,22 @@ language: php dist: bionic git: - depth: 2 + depth: 1 addons: apt_packages: - parallel - - language-pack-fr-base - zookeeperd - libzookeeper-mt-dev - - librabbitmq-dev - - libsodium-dev - - libtidy-dev - - zlib1g-dev - -env: - global: - - SYMFONY_VERSION=5.3 - - MIN_PHP=7.2.5 - - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/shims/php - - SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE=1 matrix: include: - - php: 7.2 - env: php_extra="7.3 8.0" - - php: 7.4 - env: deps=high - - php: 8.0 - env: deps=low + - php: 7.3 fast_finish: true cache: directories: - .phpunit - - php-$MIN_PHP - ~/php-ext before_install: @@ -46,14 +28,6 @@ before_install: stty cols 120 sudo sed -i 's/127\.0\.1\.1 localhost/127.0.0.1 localhost/' /etc/hosts cp .github/composer-config.json "$(composer config home)/config.json" - git config --global user.email "" - git config --global user.name "Symfony" - export PHPUNIT=$(readlink -f ./phpunit) - export PHPUNIT_X="$PHPUNIT --exclude-group tty,benchmark,intl-data" - export COMPOSER_UP='composer update --no-progress --ansi' - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n' | sort) - export SYMFONY_FEATURE_BRANCH=$(curl -s https://flex.symfony.com/versions.json | jq -r '."dev-name"') - export SYMFONY_VERSIONS=$(git ls-remote -q --heads | cut -f2 | grep -o '/[1-9][0-9]*\.[0-9].*' | sort -V) nanoseconds () { local cmd="date" @@ -70,7 +44,7 @@ before_install: # tfold is a helper to create folded reports tfold () { - local title="$PHP $1 $FLIP" + local title="$PHP $1" local fold=$(echo $title | sed -r 's/[^-_A-Za-z0-9]+/./g') shift local id=$(printf %08x $(( RANDOM * RANDOM ))) @@ -110,21 +84,8 @@ before_install: } 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 - ([[ $PHP != 7.4 ]] && phpenv global $PHP 2>/dev/null) || (cd / && wget https://storage.googleapis.com/travis-ci-language-archives/php/binaries/ubuntu/18.04/x86_64/php-$PHP.tar.bz2 -O - | tar -xj) & - done - wait - ) for PHP in $TRAVIS_PHP_VERSION $php_extra; do INI=~/.phpenv/versions/$PHP/etc/conf.d/travis.ini echo date.timezone = Europe/Paris >> $INI @@ -145,158 +106,18 @@ before_install: 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 if [[ $PHP != 8.* ]]; then tfold ext.zookeeper tpecl zookeeper-0.7.2 zookeeper.so $INI fi - tfold ext.memcached tpecl memcached-3.1.5 memcached.so $INI - tfold ext.amqp tpecl amqp-1.11.0beta amqp.so $INI - tfold ext.apcu tpecl apcu-5.1.19 apcu.so $INI - tfold ext.igbinary tpecl igbinary-3.1.6 igbinary.so $INI - tfold ext.redis tpecl redis-5.2.3 redis.so $INI "no" - tfold ext.mongodb tpecl mongodb-1.10.0alpha1 mongodb.so $INI done install: - - | - # 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 [[ ! $deps ]]; then - php .github/build-packages.php HEAD^ $SYMFONY_VERSION src/Symfony/Bridge/PhpUnit - 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^ $SYMFONY_VERSION $(find src/Symfony -mindepth 2 -type f -name composer.json -printf '%h\n' | sort) && - 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, when deps=high, the version before it is checked out and tested with the locally patched components - if [[ $deps = high && $SYMFONY_VERSION = $(echo "$SYMFONY_VERSIONS" | tail -n 1 | sed s/.//) ]]; then - export FLIP='^' - export 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 && - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -printf '%h\n' | sort) - fi - - - | - # Skip the phpunit-bridge on bugfix-branches when $deps is empty - if [[ ! $deps && $SYMFONY_VERSION != $SYMFONY_FEATURE_BRANCH ]]; then - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' -printf '%h\n' | sort) - fi - - - | - # Install symfony/flex - if [[ $deps = low ]]; then - export SYMFONY_REQUIRE='>=3.4' - else - export SYMFONY_REQUIRE=">=$SYMFONY_VERSION" - fi - composer global require --no-progress --no-scripts --no-plugins symfony/flex - - - | - # Legacy tests are skipped when deps=high and when the current branch version has not the same major version number as the next one - [[ $deps = high && $SYMFONY_VERSION = *.4 ]] && export LEGACY=,legacy - - export COMPOSER_ROOT_VERSION=$SYMFONY_VERSION.x-dev - if [[ $deps ]]; then mv composer.json.phpunit composer.json; fi - - - | - # phpinfo - phpinfo() { - phpenv global $1 - php -r 'foreach (get_loaded_extensions() as $extension) echo $extension . " " . phpversion($extension) . PHP_EOL;' - php -i - } - export -f phpinfo - - for PHP in $TRAVIS_PHP_VERSION $php_extra; do - tfold phpinfo phpinfo $PHP - done - - - | - run_tests () { - set -e - export PHP=$1 - - if [[ $PHP != 8.0* && $PHP != $TRAVIS_PHP_VERSION && $TRAVIS_PULL_REQUEST != false ]]; then - echo -e "\\n\\e[33;1mIntermediate PHP version $PHP is skipped for pull requests.\\e[0m" - return - fi - phpenv global $PHP - rm vendor/composer/package-versions-deprecated -Rf - ([[ $deps ]] && cd src/Symfony/Component/HttpFoundation; cp composer.json composer.bak; 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'" || X=1 - (cd src/Symfony/Component/HttpFoundation; mv composer.bak composer.json) - COMPONENTS=$(git diff --name-only src/ | grep composer.json || true) - - if [[ $COMPONENTS && $SYMFONY_VERSION = *.4 && $TRAVIS_PULL_REQUEST != false ]]; 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 - COMPONENTS=$(echo "$COMPONENTS" | xargs dirname | xargs -n1 -I{} bash -c "[ -e '{}/phpunit.xml.dist' ] && echo '{}'" | sort) - (cd src/Symfony/Component/HttpFoundation; composer require --dev --no-update mongodb/mongodb) - [[ ! $COMPONENTS ]] || tfold 'phpunit install' SYMFONY_PHPUNIT_REMOVE_RETURN_TYPEHINT=1 ./phpunit install - [[ ! $COMPONENTS ]] || echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && rm composer.lock vendor/ -Rf && $COMPOSER_UP && $PHPUNIT_X$LEGACY'" || X=1 - fi - - [[ ! $X ]] || (exit 1) - elif [[ $deps = low ]]; then - echo "$COMPONENTS" | parallel --gnu "tfold {} 'cd {} && $COMPOSER_UP --prefer-lowest --prefer-stable && $PHPUNIT_X'" - else - if [[ $PHP = 8.0* ]]; then - # add return types before running the test suite - sed -i 's/"\*\*\/Tests\/"//' composer.json - composer install --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 - PHPUNIT_X="$PHPUNIT_X,legacy" - fi - - echo "$COMPONENTS" | parallel --gnu "tfold {} $PHPUNIT_X {}" - - tfold src/Symfony/Component/Console.tty $PHPUNIT src/Symfony/Component/Console --group tty - tfold src/Symfony/Bridge/Twig.tty $PHPUNIT src/Symfony/Bridge/Twig --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 - } - export -f run_tests + - export COMPONENTS=$(find src/Symfony -mindepth 2 -type f -name phpunit.xml.dist -not -wholename '*/Bridge/PhpUnit/*' -printf '%h\n' | sort) + - export COMPOSER_ROOT_VERSION=$(grep ' VERSION = ' src/Symfony/Component/HttpKernel/Kernel.php | grep -P -o '[0-9]+\.[0-9]+').x-dev + - composer update --no-progress --ansi + - ./phpunit install script: - echo $TRAVIS_PHP_VERSION $php_extra | xargs -n1 bash -c '(deleteTokenBySeries($tmpSeries); - $this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed)); + $this->conn->beginTransaction(); + try { + $this->deleteTokenBySeries($tmpSeries); + $this->createNewToken(new PersistentToken($token->getClass(), $token->getUserIdentifier(), $tmpSeries, $token->getTokenValue(), $lastUsed)); + + $this->conn->commit(); + } catch (\Exception $e) { + $this->conn->rollBack(); + } } /** diff --git a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php index 7738736bcd00d..f17f4af3e3c9a 100644 --- a/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php +++ b/src/Symfony/Bridge/Monolog/Command/ServerLogCommand.php @@ -68,10 +68,9 @@ protected function configure() 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 ) ; diff --git a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php index 7479ebe448dfb..61aa8a2da2b8b 100644 --- a/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php +++ b/src/Symfony/Bridge/PhpUnit/bin/simple-phpunit.php @@ -147,12 +147,14 @@ putenv('SYMFONY_DEPRECATIONS_HELPER=disabled'); } -$COMPOSER = 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 (false === $COMPOSER = getenv('COMPOSER_BINARY')) { + $COMPOSER = 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'; +} $prevCacheDir = getenv('COMPOSER_CACHE_DIR'); if ($prevCacheDir) { 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 index db855829703e4..c98bb08a74c03 100644 --- 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 @@ -8,7 +8,7 @@ {% block action %} {% if action_url %} -{{ action_url }}: {{ action_text }} +{{ action_text }}: {{ action_url }} {% endif %} {% endblock %} diff --git a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php index 7b2740fea728d..cc1e443232933 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php +++ b/src/Symfony/Bundle/FrameworkBundle/Secrets/SodiumVault.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\FrameworkBundle\Secrets; use Symfony\Component\DependencyInjection\EnvVarLoaderInterface; +use Symfony\Component\VarExporter\VarExporter; /** * @author Tobias Schultze @@ -89,7 +90,7 @@ public function seal(string $name, string $value): void $list = $this->list(); $list[$name] = null; uksort($list, 'strnatcmp'); - file_put_contents($this->pathPrefix.'list.php', sprintf("pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" encrypted in "%s"; you can commit it.', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); } @@ -141,7 +142,7 @@ public function remove(string $name): bool $list = $this->list(); unset($list[$name]); - file_put_contents($this->pathPrefix.'list.php', sprintf("pathPrefix.'list.php', sprintf("lastMessage = sprintf('Secret "%s" removed from "%s".', $name, $this->getPrettyPath(\dirname($this->pathPrefix).\DIRECTORY_SEPARATOR)); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php index f857a3e3651ba..a0366df0a7334 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -25,6 +25,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) { diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php index 50cd4a4e46da9..ce245d5cf09b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/ObjectsProvider.php @@ -220,7 +220,7 @@ 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()); diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php index 4c2caba543e43..b844a60e7789b 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Console/Descriptor/TextDescriptorTest.php @@ -19,16 +19,6 @@ class TextDescriptorTest extends AbstractDescriptorTest { private $fileLinkFormatter = null; - protected function setUp(): void - { - putenv('COLUMNS=121'); - } - - protected function tearDown(): void - { - putenv('COLUMNS'); - } - protected function getDescriptor() { return new TextDescriptor($this->fileLinkFormatter); 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/route_1_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt index 8d86bc7be8ddb..4d4a18e5a71b8 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_1_link.txt @@ -10,7 +10,7 @@ | Method | GET|HEAD | | Requirements | name: [a-z]+ | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | -| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=68\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | | | name: Joseph | | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | | | opt1: val1 | diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt index a244b515cabbf..a690b9798d90a 100644 --- a/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Fixtures/Descriptor/route_2_link.txt @@ -10,7 +10,7 @@ | Method | PUT|POST | | Requirements | NO CUSTOM | | Class | Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\RouteStub | -| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=68\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | +| Defaults | _controller: ]8;;myeditor://open?file=[:file:]&line=58\Symfony\Bundle\FrameworkBundle\Tests\Console\Descriptor\MyController::__invoke()]8;;\ | | Options | compiler_class: Symfony\Component\Routing\RouteCompiler | | | opt1: val1 | | | opt2: val2 | diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index 93781ef3a5231..f1ce0a9aabef2 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -529,8 +529,10 @@ private function createFirewall(ContainerBuilder $container, string $id, array $ $listeners[] = new Reference('security.firewall.authenticator.'.$id); // Add authenticators to the debug:firewall command - $debugCommand = $container->getDefinition('security.command.debug_firewall'); - $debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators])); + if ($container->hasDefinition('security.command.debug_firewall')) { + $debugCommand = $container->getDefinition('security.command.debug_firewall'); + $debugCommand->replaceArgument(3, array_merge($debugCommand->getArgument(3), [$id => $authenticators])); + } } $config->replaceArgument(7, $configuredEntryPoint ?: $defaultEntryPoint); 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 9f69abcaf2d8e..910f4a7020718 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig +++ b/src/Symfony/Bundle/SecurityBundle/Resources/views/Collector/security.html.twig @@ -6,8 +6,10 @@ {% if collector.token %} {% set is_authenticated = collector.enabled and collector.authenticated %} {% set color_code = is_authenticated ? '' : 'yellow' %} + {% elseif collector.enabled %} + {% set color_code = collector.authenticatorManagerEnabled ? 'yellow' : 'red' %} {% else %} - {% set color_code = collector.enabled ? 'red' : '' %} + {% set color_code = '' %} {% endif %} {% set icon %} @@ -35,7 +37,7 @@
Authenticated - {{ is_authenticated ? 'Yes' : 'No' }} + {{ is_authenticated ? 'Yes' : 'No' }}
@@ -45,7 +47,7 @@ {% else %}
Authenticated - No + No
{% endif %} diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml index 78fdc86f8d04f..e817b48901311 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/logout_delete_cookies.xml @@ -8,7 +8,7 @@ http://symfony.com/schema/dic/security https://symfony.com/schema/dic/security/security-1.0.xsd"> - + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php index ba9fbc4c5af3a..15e218856b571 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/Functional/UserPasswordEncoderCommandTest.php @@ -31,6 +31,7 @@ class UserPasswordEncoderCommandTest extends AbstractWebTestCase { /** @var CommandTester */ private $passwordEncoderCommandTester; + private $colSize; public function testEncodePasswordEmptySalt() { @@ -316,7 +317,9 @@ public function testThrowsExceptionOnNoConfiguredEncoders() protected function setUp(): void { + $this->colSize = getenv('COLUMNS'); putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); + $kernel = $this->createKernel(['test_case' => 'PasswordEncode']); $kernel->boot(); @@ -330,11 +333,11 @@ protected function setUp(): void 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(); @@ -347,7 +350,6 @@ private function setupArgon2i() private function setupArgon2id() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'argon2id.yml']); $kernel->boot(); @@ -360,7 +362,6 @@ private function setupArgon2id() private function setupBcrypt() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); $kernel = $this->createKernel(['test_case' => 'PasswordEncode', 'root_config' => 'bcrypt.yml']); $kernel->boot(); @@ -373,7 +374,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/TwigBundle/Resources/config/twig.php b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php index 8423c67bf56e6..3bc7f66fabf85 100644 --- a/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php +++ b/src/Symfony/Bundle/TwigBundle/Resources/config/twig.php @@ -158,7 +158,7 @@ ->args([ service('twig'), service('twig.error_renderer.html.inner'), - inline_service(TwigErrorRenderer::class) + inline_service('bool') ->factory([TwigErrorRenderer::class, 'isDebug']) ->args([service('request_stack'), param('kernel.debug')]), ]) diff --git a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php index d245644e988c9..c0c738ecbfb1f 100644 --- a/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php +++ b/src/Symfony/Component/Cache/Adapter/RedisTagAwareAdapter.php @@ -23,17 +23,13 @@ use Symfony\Component\Cache\Traits\RedisTrait; /** - * Stores tag id <> cache id relationship as a Redis Set, lookup on invalidation using RENAME+SMEMBERS. + * Stores tag id <> cache id relationship as a Redis Set. * * Set (tag relation info) is stored without expiry (non-volatile), while cache always gets an expiry (volatile) even * if not set by caller. Thus if you configure redis with the right eviction policy you can be safe this tag <> cache * relationship survives eviction (cache cleanup when Redis runs out of memory). * - * Requirements: - * - Client: PHP Redis or Predis - * Note: Due to lack of RENAME support it is NOT recommended to use Cluster on Predis, instead use phpredis. - * - Server: Redis 2.8+ - * Configured with any `volatile-*` eviction policy, OR `noeviction` if it will NEVER fill up memory + * Redis server 2.8+ with any `volatile-*` eviction policy, OR `noeviction` if you're sure memory will NEVER fill up * * Design limitations: * - Max 4 billion cache keys per cache tag as limited by Redis Set datatype. @@ -49,11 +45,6 @@ class RedisTagAwareAdapter extends AbstractTagAwareAdapter { use RedisTrait; - /** - * Limits for how many keys are deleted in batch. - */ - private const BULK_DELETE_LIMIT = 10000; - /** * On cache items without a lifetime set, we set it to 100 days. This is to make sure cache items are * preferred to be evicted over tag Sets, if eviction policy is configured according to requirements. @@ -96,7 +87,7 @@ protected function doSave(array $values, int $lifetime, array $addTagData = [], { $eviction = $this->getRedisEvictionPolicy(); if ('noeviction' !== $eviction && 0 !== strpos($eviction, 'volatile-')) { - throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction)); + throw new LogicException(sprintf('Redis maxmemory-policy setting "%s" is *not* supported by RedisTagAwareAdapter, use "noeviction" or "volatile-*" eviction policies.', $eviction)); } // serialize values @@ -163,15 +154,9 @@ protected function doDeleteYieldTags(array $ids): iterable return v:sub(14, 13 + v:byte(13) + v:byte(12) * 256 + v:byte(11) * 65536) EOLUA; - if ($this->redis instanceof \Predis\ClientInterface) { - $evalArgs = [$lua, 1, &$id]; - } else { - $evalArgs = [$lua, [&$id], 1]; - } - - $results = $this->pipeline(function () use ($ids, &$id, $evalArgs) { + $results = $this->pipeline(function () use ($ids, $lua) { foreach ($ids as $id) { - yield 'eval' => $evalArgs; + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id] : [$lua, [$id], 1]; } }); @@ -189,12 +174,15 @@ protected function doDeleteYieldTags(array $ids): iterable */ protected function doDeleteTagRelations(array $tagData): bool { - $this->pipeline(static function () use ($tagData) { + $results = $this->pipeline(static function () use ($tagData) { foreach ($tagData as $tagId => $idList) { array_unshift($idList, $tagId); yield 'sRem' => $idList; } - })->rewind(); + }); + foreach ($results as $result) { + // no-op + } return true; } @@ -204,77 +192,81 @@ protected function doDeleteTagRelations(array $tagData): bool */ protected function doInvalidate(array $tagIds): bool { - if (!$this->redis instanceof \Predis\ClientInterface || !$this->redis->getConnection() instanceof PredisCluster) { - $movedTagSetIds = $this->renameKeys($this->redis, $tagIds); - } else { - $clusterConnection = $this->redis->getConnection(); - $tagIdsByConnection = new \SplObjectStorage(); - $movedTagSetIds = []; + // This script scans the set of items linked to tag: it empties the set + // and removes the linked items. When the set is still not empty after + // the scan, it means we're in cluster mode and that the linked items + // are on other nodes: we move the links to a temporary set and we + // gargage collect that set from the client side. - foreach ($tagIds as $id) { - $connection = $clusterConnection->getConnectionByKey($id); - $slot = $tagIdsByConnection[$connection] ?? $tagIdsByConnection[$connection] = new \ArrayObject(); - $slot[] = $id; - } + $lua = <<<'EOLUA' + local cursor = '0' + local id = KEYS[1] + repeat + local result = redis.call('SSCAN', id, cursor, 'COUNT', 5000); + cursor = result[1]; + local rems = {} + + for _, v in ipairs(result[2]) do + local ok, _ = pcall(redis.call, 'DEL', ARGV[1]..v) + if ok then + table.insert(rems, v) + end + end + if 0 < #rems then + redis.call('SREM', id, unpack(rems)) + end + until '0' == cursor; + + redis.call('SUNIONSTORE', '{'..id..'}'..id, id) + redis.call('DEL', id) + + return redis.call('SSCAN', '{'..id..'}'..id, '0', 'COUNT', 5000) +EOLUA; - foreach ($tagIdsByConnection as $connection) { - $slot = $tagIdsByConnection[$connection]; - $movedTagSetIds = array_merge($movedTagSetIds, $this->renameKeys(new $this->redis($connection, $this->redis->getOptions()), $slot->getArrayCopy())); + $results = $this->pipeline(function () use ($tagIds, $lua) { + if ($this->redis instanceof \Predis\ClientInterface) { + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + } elseif (\is_array($prefix = $this->redis->getOption(\Redis::OPT_PREFIX) ?? '')) { + $prefix = current($prefix); } - } - // No Sets found - if (!$movedTagSetIds) { - return false; - } - - // Now safely take the time to read the keys in each set and collect ids we need to delete - $tagIdSets = $this->pipeline(static function () use ($movedTagSetIds) { - foreach ($movedTagSetIds as $movedTagId) { - yield 'sMembers' => [$movedTagId]; + foreach ($tagIds as $id) { + yield 'eval' => $this->redis instanceof \Predis\ClientInterface ? [$lua, 1, $id, $prefix] : [$lua, [$id, $prefix], 1]; } }); - // Return combination of the temporary Tag Set ids and their values (cache ids) - $ids = array_merge($movedTagSetIds, ...iterator_to_array($tagIdSets, false)); + $lua = <<<'EOLUA' + local id = KEYS[1] + local cursor = table.remove(ARGV) + redis.call('SREM', '{'..id..'}'..id, unpack(ARGV)) - // Delete cache in chunks to avoid overloading the connection - foreach (array_chunk(array_unique($ids), self::BULK_DELETE_LIMIT) as $chunkIds) { - $this->doDelete($chunkIds); - } + return redis.call('SSCAN', '{'..id..'}'..id, cursor, 'COUNT', 5000) +EOLUA; - return true; - } + foreach ($results as $id => [$cursor, $ids]) { + while ($ids || '0' !== $cursor) { + $this->doDelete($ids); - /** - * Renames several keys in order to be able to operate on them without risk of race conditions. - * - * Filters out keys that do not exist before returning new keys. - * - * @see https://redis.io/commands/rename - * @see https://redis.io/topics/cluster-spec#keys-hash-tags - * - * @return array Filtered list of the valid moved keys (only those that existed) - */ - private function renameKeys($redis, array $ids): array - { - $newIds = []; - $uniqueToken = bin2hex(random_bytes(10)); + $evalArgs = [$id, $cursor]; + array_splice($evalArgs, 1, 0, $ids); - $results = $this->pipeline(static function () use ($ids, $uniqueToken) { - foreach ($ids as $id) { - yield 'rename' => [$id, '{'.$id.'}'.$uniqueToken]; - } - }, $redis); + if ($this->redis instanceof \Predis\ClientInterface) { + array_unshift($evalArgs, $lua, 1); + } else { + $evalArgs = [$lua, $evalArgs, 1]; + } - foreach ($results as $id => $result) { - if (true === $result || ($result instanceof Status && Status::get('OK') === $result)) { - // Only take into account if ok (key existed), will be false on phpredis if it did not exist - $newIds[] = '{'.$id.'}'.$uniqueToken; + $results = $this->pipeline(function () use ($evalArgs) { + yield 'eval' => $evalArgs; + }); + + foreach ($results as [$cursor, $ids]) { + // no-op + } } } - return $newIds; + return true; } private function getRedisEvictionPolicy(): string diff --git a/src/Symfony/Component/Cache/LockRegistry.php b/src/Symfony/Component/Cache/LockRegistry.php index 38c0a6cea7b19..ecedac8c07e87 100644 --- a/src/Symfony/Component/Cache/LockRegistry.php +++ b/src/Symfony/Component/Cache/LockRegistry.php @@ -27,7 +27,7 @@ final class LockRegistry { private static $openedFiles = []; - private static $lockedFiles = []; + private static $lockedFiles; /** * The number of items in this list controls the max number of concurrent processes. @@ -82,6 +82,11 @@ public static function setFiles(array $files): array public static function compute(callable $callback, ItemInterface $item, bool &$save, CacheInterface $pool, \Closure $setMetadata = null, LoggerInterface $logger = null) { + if ('\\' === \DIRECTORY_SEPARATOR && null === self::$lockedFiles) { + // disable locking on Windows by default + self::$files = self::$lockedFiles = []; + } + $key = self::$files ? abs(crc32($item->getKey())) % \count(self::$files) : -1; if ($key < 0 || (self::$lockedFiles[$key] ?? false) || !$lock = self::open($key)) { diff --git a/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php index 994ae81d5b3a6..9d14007fde75f 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/AbstractRedisAdapterTest.php @@ -24,7 +24,7 @@ abstract class AbstractRedisAdapterTest extends AdapterTestCase protected static $redis; - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { return new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); } @@ -45,4 +45,18 @@ public static function tearDownAfterClass(): void { self::$redis = null; } + + /** + * @runInSeparateProcess + */ + public function testClearWithPrefix() + { + $cache = $this->createCachePool(0, __FUNCTION__); + + $cache->save($cache->getItem('foo')->set('bar')); + $this->assertTrue($cache->hasItem('foo')); + + $cache->clear(); + $this->assertFalse($cache->hasItem('foo')); + } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php index e19f74f6745c2..a1a2b4dda3fc8 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisAdapterTest.php @@ -22,7 +22,7 @@ class PredisAdapterTest extends AbstractRedisAdapterTest public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$redis = new \Predis\Client(['host' => getenv('REDIS_HOST')]); + self::$redis = new \Predis\Client(['host' => getenv('REDIS_HOST')], ['prefix' => 'prefix_']); } public function testCreateConnection() diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php index e6989be292334..e2f09cd23ae44 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisClusterAdapterTest.php @@ -19,7 +19,7 @@ class PredisClusterAdapterTest extends AbstractRedisAdapterTest public static function setUpBeforeClass(): void { parent::setUpBeforeClass(); - self::$redis = new \Predis\Client([['host' => getenv('REDIS_HOST')]]); + self::$redis = new \Predis\Client([['host' => getenv('REDIS_HOST')]], ['prefix' => 'prefix_']); } public static function tearDownAfterClass(): void diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisClusterAdapterTest.php index 81dd0bc2a04cc..9db83c0db4126 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisClusterAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisRedisClusterAdapterTest.php @@ -24,7 +24,7 @@ public static function setUpBeforeClass(): void self::markTestSkipped('REDIS_CLUSTER_HOSTS env var is not defined.'); } - self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['class' => \Predis\Client::class, 'redis_cluster' => true]); + self::$redis = RedisAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['class' => \Predis\Client::class, 'redis_cluster' => true, 'prefix' => 'prefix_']); } public static function tearDownAfterClass(): void diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php index 6cffbde7926f1..0971f80c553e5 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareAdapterTest.php @@ -27,7 +27,7 @@ protected function setUp(): void $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { $this->assertInstanceOf(\Predis\Client::class, self::$redis); $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php index 21120d606ac18..af25b2df52c45 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/PredisTagAwareClusterAdapterTest.php @@ -27,7 +27,7 @@ protected function setUp(): void $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { $this->assertInstanceOf(\Predis\Client::class, self::$redis); $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php index b28936ee6814f..a3100edd8abe1 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterSentinelTest.php @@ -32,7 +32,7 @@ public static function setUpBeforeClass(): void self::markTestSkipped('REDIS_SENTINEL_SERVICE env var is not defined.'); } - self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['redis_sentinel' => $service]); + self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['redis_sentinel' => $service, 'prefix' => 'prefix_']); } public function testInvalidDSNHasBothClusterAndSentinel() diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php index b54a5acc84260..d961187aeca3a 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisAdapterTest.php @@ -28,9 +28,13 @@ public static function setUpBeforeClass(): void self::$redis = AbstractAdapter::createConnection('redis://'.getenv('REDIS_HOST'), ['lazy' => true]); } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { - $adapter = parent::createCachePool($defaultLifetime); + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + + $adapter = parent::createCachePool($defaultLifetime, $testMethod); $this->assertInstanceOf(RedisProxy::class, self::$redis); return $adapter; diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php index 70afe6dac97c9..6e0b448746e86 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisArrayAdapterTest.php @@ -23,5 +23,6 @@ public static function setUpBeforeClass(): void self::markTestSkipped('The RedisArray class is required.'); } self::$redis = new \RedisArray([getenv('REDIS_HOST')], ['lazy_connect' => true]); + self::$redis->setOption(\Redis::OPT_PREFIX, 'prefix_'); } } diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisClusterAdapterTest.php index 1253aeb5007a7..011a36b338229 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisClusterAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisClusterAdapterTest.php @@ -32,10 +32,15 @@ public static function setUpBeforeClass(): void } self::$redis = AbstractAdapter::createConnection('redis:?host['.str_replace(' ', ']&host[', $hosts).']', ['lazy' => true, 'redis_cluster' => true]); + self::$redis->setOption(\Redis::OPT_PREFIX, 'prefix_'); } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + $this->assertInstanceOf(RedisClusterProxy::class, self::$redis); $adapter = new RedisAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php index 5c82016be2adb..12e3b6ff55365 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareAdapterTest.php @@ -28,8 +28,12 @@ protected function setUp(): void $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + $this->assertInstanceOf(RedisProxy::class, self::$redis); $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php index 3ec500a9010e9..b5823711dc858 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareArrayAdapterTest.php @@ -27,8 +27,12 @@ protected function setUp(): void $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + $this->assertInstanceOf(\RedisArray::class, self::$redis); $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php index 50f078c04d4b0..d4a1bc97779ca 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/RedisTagAwareClusterAdapterTest.php @@ -28,8 +28,12 @@ protected function setUp(): void $this->skippedTests['testTagItemExpiry'] = 'Testing expiration slows down the test suite'; } - public function createCachePool(int $defaultLifetime = 0): CacheItemPoolInterface + public function createCachePool(int $defaultLifetime = 0, string $testMethod = null): CacheItemPoolInterface { + if ('testClearWithPrefix' === $testMethod && \defined('Redis::SCAN_PREFIX')) { + self::$redis->setOption(\Redis::OPT_SCAN, \Redis::SCAN_PREFIX); + } + $this->assertInstanceOf(RedisClusterProxy::class, self::$redis); $adapter = new RedisTagAwareAdapter(self::$redis, str_replace('\\', '.', __CLASS__), $defaultLifetime); diff --git a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php index 9a45adaa36e2b..83696785fd927 100644 --- a/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php +++ b/src/Symfony/Component/Cache/Tests/Adapter/TagAwareAdapterTest.php @@ -19,6 +19,7 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter; +use Symfony\Component\Cache\LockRegistry; use Symfony\Component\Cache\Tests\Fixtures\PrunableAdapter; use Symfony\Component\Filesystem\Filesystem; @@ -200,6 +201,8 @@ public function testGetItemReturnsCacheMissWhenPoolDoesNotHaveItemAndOnlyHasTags public function testLog() { + $lockFiles = LockRegistry::setFiles([__FILE__]); + $logger = $this->createMock(LoggerInterface::class); $logger ->expects($this->atLeastOnce()) @@ -210,6 +213,8 @@ public function testLog() // Computing will produce at least one log $cache->get('foo', static function (): string { return 'ccc'; }); + + LockRegistry::setFiles($lockFiles); } /** diff --git a/src/Symfony/Component/Cache/Tests/LockRegistryTest.php b/src/Symfony/Component/Cache/Tests/LockRegistryTest.php index 0771347ed6fe3..30ff6774047a5 100644 --- a/src/Symfony/Component/Cache/Tests/LockRegistryTest.php +++ b/src/Symfony/Component/Cache/Tests/LockRegistryTest.php @@ -18,6 +18,9 @@ class LockRegistryTest extends TestCase { public function testFiles() { + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('LockRegistry is disabled on Windows'); + } $lockFiles = LockRegistry::setFiles([]); LockRegistry::setFiles($lockFiles); $expected = array_map('realpath', glob(__DIR__.'/../Adapter/*')); diff --git a/src/Symfony/Component/Cache/Traits/RedisClusterNodeProxy.php b/src/Symfony/Component/Cache/Traits/RedisClusterNodeProxy.php index 7818f0b8df9c9..deba74f6a3b7d 100644 --- a/src/Symfony/Component/Cache/Traits/RedisClusterNodeProxy.php +++ b/src/Symfony/Component/Cache/Traits/RedisClusterNodeProxy.php @@ -45,4 +45,9 @@ public function scan(&$iIterator, $strPattern = null, $iCount = null) { return $this->redis->scan($iIterator, $this->host, $strPattern, $iCount); } + + public function getOption($name) + { + return $this->redis->getOption($name); + } } diff --git a/src/Symfony/Component/Cache/Traits/RedisTrait.php b/src/Symfony/Component/Cache/Traits/RedisTrait.php index 8f11e846126de..618a2c3470007 100644 --- a/src/Symfony/Component/Cache/Traits/RedisTrait.php +++ b/src/Symfony/Component/Cache/Traits/RedisTrait.php @@ -374,13 +374,12 @@ protected function doHave(string $id) */ protected function doClear(string $namespace) { - $cleared = true; if ($this->redis instanceof \Predis\ClientInterface) { - $evalArgs = [0, $namespace]; - } else { - $evalArgs = [[$namespace], 0]; + $prefix = $this->redis->getOptions()->prefix ? $this->redis->getOptions()->prefix->getPrefix() : ''; + $prefixLen = \strlen($prefix); } + $cleared = true; $hosts = $this->getHosts(); $host = reset($hosts); if ($host instanceof \Predis\Client && $host->getConnection() instanceof ReplicationInterface) { @@ -397,23 +396,35 @@ protected function doClear(string $namespace) $info = $host->info('Server'); $info = $info['Server'] ?? $info; + if (!$host instanceof \Predis\ClientInterface) { + $prefix = \defined('Redis::SCAN_PREFIX') && (\Redis::SCAN_PREFIX & $host->getOption(\Redis::OPT_SCAN)) ? '' : $host->getOption(\Redis::OPT_PREFIX); + $prefixLen = \strlen($host->getOption(\Redis::OPT_PREFIX) ?? ''); + } + $pattern = $prefix.$namespace.'*'; + if (!version_compare($info['redis_version'], '2.8', '>=')) { // As documented in Redis documentation (http://redis.io/commands/keys) using KEYS // can hang your server when it is executed against large databases (millions of items). // Whenever you hit this scale, you should really consider upgrading to Redis 2.8 or above. $unlink = version_compare($info['redis_version'], '4.0', '>=') ? 'UNLINK' : 'DEL'; - $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]..'*') for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $evalArgs[0], $evalArgs[1]) && $cleared; + $args = $this->redis instanceof \Predis\ClientInterface ? [0, $pattern] : [[$pattern], 0]; + $cleared = $host->eval("local keys=redis.call('KEYS',ARGV[1]) for i=1,#keys,5000 do redis.call('$unlink',unpack(keys,i,math.min(i+4999,#keys))) end return 1", $args[0], $args[1]) && $cleared; continue; } $cursor = null; do { - $keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor, 'MATCH', $namespace.'*', 'COUNT', 1000) : $host->scan($cursor, $namespace.'*', 1000); + $keys = $host instanceof \Predis\ClientInterface ? $host->scan($cursor, 'MATCH', $pattern, 'COUNT', 1000) : $host->scan($cursor, $pattern, 1000); if (isset($keys[1]) && \is_array($keys[1])) { $cursor = $keys[0]; $keys = $keys[1]; } if ($keys) { + if ($prefixLen) { + foreach ($keys as $i => $key) { + $keys[$i] = substr($key, $prefixLen); + } + } $this->doDelete($keys); } } while ($cursor = (int) $cursor); @@ -535,6 +546,11 @@ private function pipeline(\Closure $generator, $redis = null): \Generator $results = $redis->exec(); } + if (!$redis instanceof \Predis\ClientInterface && 'eval' === $command && $redis->getLastError()) { + $e = new \RedisException($redis->getLastError()); + $results = array_map(function ($v) use ($e) { return false === $v ? $e : $v; }, $results); + } + foreach ($ids as $k => $id) { yield $id => $results[$k]; } diff --git a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php index c23bd6a8c049f..4bc6903cdf8e1 100644 --- a/src/Symfony/Component/Config/Resource/ReflectionClassResource.php +++ b/src/Symfony/Component/Config/Resource/ReflectionClassResource.php @@ -163,6 +163,8 @@ private function generateSignature(\ReflectionClass $class): iterable } } + $defined = \Closure::bind(static function ($c) { return \defined($c); }, null, $class->name); + foreach ($class->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED) as $m) { if (\PHP_VERSION_ID >= 80000) { foreach ($m->getAttributes() as $a) { @@ -189,7 +191,7 @@ private function generateSignature(\ReflectionClass $class): iterable continue; } - if (!$p->isDefaultValueConstant() || \defined($p->getDefaultValueConstantName())) { + if (!$p->isDefaultValueConstant() || $defined($p->getDefaultValueConstantName())) { $defaults[$p->name] = $p->getDefaultValue(); continue; diff --git a/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php index 4f61c5bbd7c61..8ba3a8c2c93cf 100644 --- a/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php +++ b/src/Symfony/Component/Config/Tests/Resource/ReflectionClassResourceTest.php @@ -164,6 +164,7 @@ public function provideHashedSignature(): iterable yield [true, 17, 'public function ccc($bar = 187) {}']; yield [true, 17, 'public function ccc($bar = ANOTHER_ONE_THAT_WILL_NEVER_BE_DEFINED_CCCCCCCCC) {}']; + yield [true, 17, 'public function ccc($bar = parent::BOOM) {}']; yield [true, 17, null, static function () { \define('A_CONSTANT_THAT_FOR_SURE_WILL_NEVER_BE_DEFINED_CCCCCC', 'foo'); }]; } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php index bb87f47cddd47..c776195e2d6c6 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/CheckDefinitionValidityPass.php @@ -44,7 +44,7 @@ public function process(ContainerBuilder $container) } // non-synthetic, non-abstract service has class - if (!$definition->isAbstract() && !$definition->isSynthetic() && !$definition->getClass() && (!$definition->getFactory() || !preg_match(FileLoader::ANONYMOUS_ID_REGEXP, $id))) { + if (!$definition->isAbstract() && !$definition->isSynthetic() && !$definition->getClass() && !$definition->hasTag('container.service_locator') && (!$definition->getFactory() || !preg_match(FileLoader::ANONYMOUS_ID_REGEXP, $id))) { if ($definition->getFactory()) { throw new RuntimeException(sprintf('Please add the class to service "%s" even if it is constructed by a factory since we might need to add method calls based on compile-time checks.', $id)); } diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php index 85c2f214a1a90..3b971db2c75d8 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php @@ -14,6 +14,7 @@ use Symfony\Component\DependencyInjection\Alias; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Reference; @@ -74,6 +75,7 @@ public function process(ContainerBuilder $container) $public = $alias->isPublic(); $private = $alias->isPrivate(); $container->setAlias($renamedId, new Alias((string) $alias, false)); + $decoratedDefinition = $container->findDefinition($alias); } elseif ($container->hasDefinition($inner)) { $decoratedDefinition = $container->getDefinition($inner); $public = $decoratedDefinition->isPublic(); @@ -87,10 +89,15 @@ public function process(ContainerBuilder $container) } elseif (ContainerInterface::NULL_ON_INVALID_REFERENCE === $invalidBehavior) { $public = $definition->isPublic(); $private = $definition->isPrivate(); + $decoratedDefinition = null; } else { throw new ServiceNotFoundException($inner, $id); } + if ($decoratedDefinition && $decoratedDefinition->isSynthetic()) { + throw new InvalidArgumentException(sprintf('A synthetic service cannot be decorated: service "%s" cannot decorate "%s".', $id, $inner)); + } + if (isset($decoratingDefinitions[$inner])) { $decoratingDefinition = $decoratingDefinitions[$inner]; diff --git a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php index ea0ee96c2a278..4b4c5022672aa 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/ResolveBindingsPass.php @@ -135,7 +135,7 @@ protected function processValue($value, bool $isRoot = false) } if (null !== $bindingValue && !$bindingValue instanceof Reference && !$bindingValue instanceof Definition && !$bindingValue instanceof TaggedIteratorArgument && !$bindingValue instanceof ServiceLocatorArgument) { - throw new InvalidArgumentException(sprintf('Invalid value for binding key "%s" for service "%s": expected null, "%s", "%s", "%s" or ServiceLocatorArgument, "%s" given.', $key, $this->currentId, Reference::class, Definition::class, TaggedIteratorArgument::class, get_debug_type($bindingValue))); + throw new InvalidArgumentException(sprintf('Invalid value for binding key "%s" for service "%s": expected "%s", "%s", "%s", "%s" or null, "%s" given.', $key, $this->currentId, Reference::class, Definition::class, TaggedIteratorArgument::class, ServiceLocatorArgument::class, get_debug_type($bindingValue))); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 8d0bc6cbf126c..82e167cde0cab 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -1871,6 +1871,8 @@ private function dumpValue($value, bool $interpolate = true): string return $code; } + } elseif ($value instanceof \UnitEnum) { + return sprintf('\%s::%s', \get_class($value), $value->name); } elseif ($value instanceof AbstractArgument) { throw new RuntimeException($value->getTextWithContext()); } elseif (\is_object($value) || \is_resource($value)) { diff --git a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php index 9c950c75edd6a..a04f75a7fd041 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/XmlDumper.php @@ -324,6 +324,9 @@ private function convertParameters(array $parameters, string $type, \DOMElement $element->setAttribute('type', 'binary'); $text = $this->document->createTextNode(self::phpToXml(base64_encode($value))); $element->appendChild($text); + } elseif ($value instanceof \UnitEnum) { + $element->setAttribute('type', 'constant'); + $element->appendChild($this->document->createTextNode(self::phpToXml($value))); } elseif ($value instanceof AbstractArgument) { $element->setAttribute('type', 'abstract'); $text = $this->document->createTextNode(self::phpToXml($value->getText())); @@ -381,6 +384,8 @@ public static function phpToXml($value): string return 'false'; case $value instanceof Parameter: return '%'.$value.'%'; + case $value instanceof \UnitEnum: + return sprintf('%s::%s', \get_class($value), $value->name); case \is_object($value) || \is_resource($value): throw new RuntimeException('Unable to dump a service container if a parameter is an object or a resource.'); default: diff --git a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php index cf40ec0bf47a6..98617a66da39a 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/YamlDumper.php @@ -306,6 +306,8 @@ private function dumpValue($value) return $this->getExpressionCall((string) $value); } elseif ($value instanceof Definition) { return new TaggedValue('service', (new Parser())->parse("_:\n".$this->addService('_', $value), Yaml::PARSE_CUSTOM_TAGS)['_']['_']); + } elseif ($value instanceof \UnitEnum) { + return new TaggedValue('php/const', sprintf('%s::%s', \get_class($value), $value->name)); } elseif ($value instanceof AbstractArgument) { return new TaggedValue('abstract', $value->getText()); } elseif (\is_object($value) || \is_resource($value)) { diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php index 3d16ad6f01c24..573b6f53a291d 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/BindTrait.php @@ -34,7 +34,7 @@ trait BindTrait final public function bind(string $nameOrFqcn, $valueOrRef): self { $valueOrRef = static::processValue($valueOrRef, true); - if (!preg_match('/^(?:(?:array|bool|float|int|string)[ \t]*+)?\$/', $nameOrFqcn) && !$valueOrRef instanceof Reference) { + if (!preg_match('/^(?:(?:array|bool|float|int|string|iterable)[ \t]*+)?\$/', $nameOrFqcn) && !$valueOrRef instanceof Reference) { throw new InvalidArgumentException(sprintf('Invalid binding for service "%s": named arguments must start with a "$", and FQCN must map to references. Neither applies to binding "%s".', $this->id, $nameOrFqcn)); } $bindings = $this->definition->getBindings(); diff --git a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php index 0b376bf041539..1286ba4c1e352 100644 --- a/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php +++ b/src/Symfony/Component/DependencyInjection/Loader/Configurator/Traits/FactoryTrait.php @@ -12,13 +12,14 @@ namespace Symfony\Component\DependencyInjection\Loader\Configurator\Traits; use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; +use Symfony\Component\DependencyInjection\Loader\Configurator\ReferenceConfigurator; trait FactoryTrait { /** * Sets a factory. * - * @param string|array $factory A PHP callable reference + * @param string|array|ReferenceConfigurator $factory A PHP callable reference * * @return $this */ diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php index ed1e300ce053c..c683fdbbc118a 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/CheckDefinitionValidityPassTest.php @@ -37,6 +37,16 @@ public function testProcessDetectsNonSyntheticNonAbstractDefinitionWithoutClass( $this->process($container); } + public function testProcessAcceptsServiceLocatorWithoutClass() + { + $container = new ContainerBuilder(); + $container->register('a')->addTag('container.service_locator'); + + $this->process($container); + + $this->addToAssertionCount(1); + } + public function testProcessDetectsFactoryWithoutClass() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php index 77f0742acd3a2..5f7bd8cfbe7d2 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php @@ -16,6 +16,7 @@ use Symfony\Component\DependencyInjection\Compiler\DecoratorServicePass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerInterface; +use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException; use Symfony\Component\DependencyInjection\Exception\ServiceNotFoundException; use Symfony\Component\DependencyInjection\Reference; @@ -261,6 +262,23 @@ public function testProcessLeavesServiceSubscriberTagOnOriginalDefinition() $this->assertEquals(['bar' => ['attr' => 'baz'], 'foobar' => ['attr' => 'bar']], $container->getDefinition('baz')->getTags()); } + public function testCannotDecorateSyntheticService() + { + $container = new ContainerBuilder(); + $container + ->register('foo') + ->setSynthetic(true) + ; + $container + ->register('baz') + ->setDecoratedService('foo') + ; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('A synthetic service cannot be decorated: service "baz" cannot decorate "foo".'); + $this->process($container); + } + public function testGenericInnerReference() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php index 4e4eda2213b8d..e87bf4ae84dc9 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/PhpDumperTest.php @@ -42,6 +42,8 @@ use Symfony\Component\DependencyInjection\Tests\Compiler\Foo; use Symfony\Component\DependencyInjection\Tests\Compiler\Wither; use Symfony\Component\DependencyInjection\Tests\Fixtures\CustomDefinition; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; use Symfony\Component\DependencyInjection\Tests\Fixtures\ScalarFactory; use Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator; @@ -1228,6 +1230,29 @@ public function testDumpHandlesObjectClassNames() $this->assertInstanceOf(\stdClass::class, $container->get('bar')); } + /** + * @requires PHP 8.1 + */ + public function testDumpHandlesEnumeration() + { + $container = new ContainerBuilder(); + $container + ->register('foo', FooClassWithEnumAttribute::class) + ->setPublic(true) + ->addArgument(FooUnitEnum::BAR); + + $container->compile(); + + $dumper = new PhpDumper($container); + eval('?>'.$dumper->dump([ + 'class' => 'Symfony_DI_PhpDumper_Test_Enumeration', + ])); + + $container = new \Symfony_DI_PhpDumper_Test_Enumeration(); + + $this->assertSame(FooUnitEnum::BAR, $container->get('foo')->getBar()); + } + public function testUninitializedSyntheticReference() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php index 83b4cf63d293d..7521cdb0be140 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/XmlDumperTest.php @@ -22,6 +22,8 @@ use Symfony\Component\DependencyInjection\Dumper\XmlDumper; use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; class XmlDumperTest extends TestCase @@ -272,6 +274,23 @@ public function testDumpAbstractServices() $this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services_abstract.xml'), $dumper->dump()); } + /** + * @requires PHP 8.1 + */ + public function testDumpHandlesEnumeration() + { + $container = new ContainerBuilder(); + $container + ->register(FooClassWithEnumAttribute::class, FooClassWithEnumAttribute::class) + ->setPublic(true) + ->addArgument(FooUnitEnum::BAR); + + $container->compile(); + $dumper = new XmlDumper($container); + + $this->assertEquals(file_get_contents(self::$fixturesPath.'/xml/services_with_enumeration.xml'), $dumper->dump()); + } + public function testDumpServiceWithAbstractArgument() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php index 793002eebbad5..0fed70bb0926d 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Dumper/YamlDumperTest.php @@ -23,6 +23,8 @@ use Symfony\Component\DependencyInjection\Dumper\YamlDumper; use Symfony\Component\DependencyInjection\Loader\YamlFileLoader; use Symfony\Component\DependencyInjection\Reference; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; use Symfony\Component\Yaml\Parser; use Symfony\Component\Yaml\Yaml; @@ -131,6 +133,23 @@ public function testServiceClosure() $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_service_closure.yml', $dumper->dump()); } + /** + * @requires PHP 8.1 + */ + public function testDumpHandlesEnumeration() + { + $container = new ContainerBuilder(); + $container + ->register(FooClassWithEnumAttribute::class, FooClassWithEnumAttribute::class) + ->setPublic(true) + ->addArgument(FooUnitEnum::BAR); + + $container->compile(); + $dumper = new YamlDumper($container); + + $this->assertEquals(file_get_contents(self::$fixturesPath.'/yaml/services_with_enumeration.yml'), $dumper->dump()); + } + public function testDumpServiceWithAbstractArgument() { $container = new ContainerBuilder(); @@ -142,7 +161,6 @@ public function testDumpServiceWithAbstractArgument() $this->assertStringEqualsFile(self::$fixturesPath.'/yaml/services_with_abstract_argument.yml', $dumper->dump()); } - private function assertEqualYamlStructure(string $expected, string $yaml, string $message = '') { $parser = new Parser(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooClassWithEnumAttribute.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooClassWithEnumAttribute.php new file mode 100644 index 0000000000000..3b2235efdd76b --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooClassWithEnumAttribute.php @@ -0,0 +1,18 @@ +bar = $bar; + } + + public function getBar(): FooUnitEnum + { + return $this->bar; + } +} diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooUnitEnum.php b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooUnitEnum.php new file mode 100644 index 0000000000000..d51cf9c995e26 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/FooUnitEnum.php @@ -0,0 +1,8 @@ +autowire() ->tag('t', ['a' => 'b']) ->bind(Foo::class, service('bar')) + ->bind('iterable $foo', tagged_iterator('foo')) ->public(); $s->set(Foo::class)->args([service('bar')])->public(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_enumeration.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_enumeration.xml new file mode 100644 index 0000000000000..30e80f0053d2d --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_enumeration.xml @@ -0,0 +1,9 @@ + + + + + + Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAR + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_invalid_enumeration.xml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_invalid_enumeration.xml new file mode 100644 index 0000000000000..8864e6d892857 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/xml/services_with_invalid_enumeration.xml @@ -0,0 +1,9 @@ + + + + + + Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAZ + + + diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_enumeration.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_enumeration.yml new file mode 100644 index 0000000000000..46bf505d44b80 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_enumeration.yml @@ -0,0 +1,10 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute + public: true + arguments: [!php/const 'Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAR'] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_invalid_enumeration.yml b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_invalid_enumeration.yml new file mode 100644 index 0000000000000..b9f74e0f468ab --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Tests/Fixtures/yaml/services_with_invalid_enumeration.yml @@ -0,0 +1,10 @@ + +services: + service_container: + class: Symfony\Component\DependencyInjection\ContainerInterface + public: true + synthetic: true + Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute: + class: Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute + public: true + arguments: [!php/const 'Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAZ'] diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php index c20b2311b8337..93babd01043fe 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/XmlFileLoaderTest.php @@ -39,6 +39,8 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar; use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\FooWithAbstractArgument; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; @@ -905,6 +907,32 @@ public function testInstanceof() $this->assertSame(['foo' => [[]], 'bar' => [[]]], $definition->getTags()); } + /** + * @requires PHP 8.1 + */ + public function testEnumeration() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + $loader->load('services_with_enumeration.xml'); + $container->compile(); + + $definition = $container->getDefinition(FooClassWithEnumAttribute::class); + $this->assertSame([FooUnitEnum::BAR], $definition->getArguments()); + } + + /** + * @requires PHP 8.1 + */ + public function testInvalidEnumeration() + { + $container = new ContainerBuilder(); + $loader = new XmlFileLoader($container, new FileLocator(self::$fixturesPath.'/xml')); + + $this->expectException(\Error::class); + $loader->load('services_with_invalid_enumeration.xml'); + } + public function testInstanceOfAndChildDefinition() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php index 0ab8c39993c23..55b0a8acb53c0 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Loader/YamlFileLoaderTest.php @@ -38,6 +38,8 @@ use Symfony\Component\DependencyInjection\Tests\Fixtures\Bar; use Symfony\Component\DependencyInjection\Tests\Fixtures\BarInterface; use Symfony\Component\DependencyInjection\Tests\Fixtures\CaseSensitiveClass; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooClassWithEnumAttribute; +use Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum; use Symfony\Component\DependencyInjection\Tests\Fixtures\NamedArgumentsDummy; use Symfony\Component\DependencyInjection\Tests\Fixtures\Prototype; use Symfony\Component\ExpressionLanguage\Expression; @@ -957,6 +959,33 @@ public function testDefaultValueOfTagged() $this->assertNull($iteratorArgument->getIndexAttribute()); } + /** + * @requires PHP 8.1 + */ + public function testEnumeration() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + $loader->load('services_with_enumeration.yml'); + $container->compile(); + + $definition = $container->getDefinition(FooClassWithEnumAttribute::class); + $this->assertSame([FooUnitEnum::BAR], $definition->getArguments()); + } + + /** + * @requires PHP 8.1 + */ + public function testInvalidEnumeration() + { + $container = new ContainerBuilder(); + $loader = new YamlFileLoader($container, new FileLocator(self::$fixturesPath.'/yaml')); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The constant "Symfony\Component\DependencyInjection\Tests\Fixtures\FooUnitEnum::BAZ" is not defined'); + $loader->load('services_with_invalid_enumeration.yml'); + } + public function testReturnsClone() { $container = new ContainerBuilder(); diff --git a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php index 8f7ecb4017b61..561f9b57402c5 100644 --- a/src/Symfony/Component/ErrorHandler/DebugClassLoader.php +++ b/src/Symfony/Component/ErrorHandler/DebugClassLoader.php @@ -15,6 +15,7 @@ use Doctrine\Common\Persistence\Proxy as LegacyProxy; use Doctrine\Persistence\Proxy; use Mockery\MockInterface; +use Phake\IMock; use PHPUnit\Framework\MockObject\Matcher\StatelessInvocation; use PHPUnit\Framework\MockObject\MockObject; use Prophecy\Prophecy\ProphecySubjectInterface; @@ -310,6 +311,7 @@ public static function checkClasses(): bool && !is_subclass_of($symbols[$i], ProxyInterface::class) && !is_subclass_of($symbols[$i], LegacyProxy::class) && !is_subclass_of($symbols[$i], MockInterface::class) + && !is_subclass_of($symbols[$i], IMock::class) ) { $loader->checkClass($symbols[$i]); } diff --git a/src/Symfony/Component/ErrorHandler/Resources/views/logs.html.php b/src/Symfony/Component/ErrorHandler/Resources/views/logs.html.php index 9757e1a61151f..ea6e727b8cd01 100644 --- a/src/Symfony/Component/ErrorHandler/Resources/views/logs.html.php +++ b/src/Symfony/Component/ErrorHandler/Resources/views/logs.html.php @@ -17,7 +17,7 @@ $status = 'warning'; } else { $severity = 0; - if (($exception = $log['context']['exception'] ?? null) instanceof \ErrorException) { + if (($exception = $log['context']['exception'] ?? null) instanceof \ErrorException || $exception instanceof \Symfony\Component\ErrorHandler\Exception\SilencedErrorContext) { $severity = $exception->getSeverity(); } $status = \E_DEPRECATED === $severity || \E_USER_DEPRECATED === $severity ? 'warning' : 'normal'; diff --git a/src/Symfony/Component/ErrorHandler/ThrowableUtils.php b/src/Symfony/Component/ErrorHandler/ThrowableUtils.php index d6efcbefa0cc0..18d04988ac342 100644 --- a/src/Symfony/Component/ErrorHandler/ThrowableUtils.php +++ b/src/Symfony/Component/ErrorHandler/ThrowableUtils.php @@ -11,14 +11,19 @@ namespace Symfony\Component\ErrorHandler; +use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; + /** * @internal */ class ThrowableUtils { - public static function getSeverity(\Throwable $throwable): int + /** + * @param SilencedErrorContext|\Throwable + */ + public static function getSeverity($throwable): int { - if ($throwable instanceof \ErrorException) { + if ($throwable instanceof \ErrorException || $throwable instanceof SilencedErrorContext) { return $throwable->getSeverity(); } diff --git a/src/Symfony/Component/Filesystem/Filesystem.php b/src/Symfony/Component/Filesystem/Filesystem.php index d4083feef6e24..aff19d14aa375 100644 --- a/src/Symfony/Component/Filesystem/Filesystem.php +++ b/src/Symfony/Component/Filesystem/Filesystem.php @@ -669,10 +669,6 @@ public function dumpFile(string $filename, $content) $this->mkdir($dir); } - if (!is_writable($dir)) { - throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); - } - // Will create a temp file with 0600 access rights // when the filesystem supports chmod. $tmpFile = $this->tempnam($dir, basename($filename)); @@ -711,10 +707,6 @@ public function appendToFile(string $filename, $content) $this->mkdir($dir); } - if (!is_writable($dir)) { - throw new IOException(sprintf('Unable to write to the "%s" directory.', $dir), 0, null, $dir); - } - if (false === self::box('file_put_contents', $filename, $content, \FILE_APPEND)) { throw new IOException(sprintf('Failed to write file "%s": ', $filename).self::$lastError, 0, null, $filename); } diff --git a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php index bedb2385e73e8..5090decad5bd4 100644 --- a/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php +++ b/src/Symfony/Component/Filesystem/Tests/FilesystemTest.php @@ -1766,6 +1766,27 @@ public function testCopyShouldKeepExecutionPermission() $this->assertFilePermissions(767, $targetFilePath); } + public function testDumpToProtectedDirectory() + { + if (\DIRECTORY_SEPARATOR !== '\\') { + $this->markTestSkipped('This test is specific to Windows.'); + } + + if (($userProfilePath = getenv('USERPROFILE')) === false || !is_dir($userProfilePath)) { + throw new \RuntimeException('Failed to retrieve user profile path.'); + } + + $targetPath = implode(\DIRECTORY_SEPARATOR, [$userProfilePath, 'Downloads', '__test_file.ext']); + + try { + $this->assertFileDoesNotExist($targetPath); + $this->filesystem->dumpFile($targetPath, 'foobar'); + $this->assertFileExists($targetPath); + } finally { + $this->filesystem->remove($targetPath); + } + } + /** * Normalize the given path (transform each forward slash into a real directory separator). */ diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php index e5f5d1228b5dc..bc870494e6461 100644 --- a/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php +++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/AbstractDescriptorTest.php @@ -28,6 +28,19 @@ abstract class AbstractDescriptorTest extends TestCase { + private $colSize; + + protected function setUp(): void + { + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); + } + + protected function tearDown(): void + { + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); + } + /** @dataProvider getDescribeDefaultsTestData */ public function testDescribeDefaults($object, array $options, $fixtureName) { diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php index 5926fe527738f..c035be6bcd9cf 100644 --- a/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php +++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/JsonDescriptorTest.php @@ -15,16 +15,6 @@ class JsonDescriptorTest extends AbstractDescriptorTest { - protected function setUp(): void - { - putenv('COLUMNS=121'); - } - - protected function tearDown(): void - { - putenv('COLUMNS'); - } - protected function getDescriptor() { return new JsonDescriptor(); diff --git a/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php b/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php index ed1582e6b21ba..c970eba96e62f 100644 --- a/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php +++ b/src/Symfony/Component/Form/Tests/Console/Descriptor/TextDescriptorTest.php @@ -15,16 +15,6 @@ class TextDescriptorTest extends AbstractDescriptorTest { - protected function setUp(): void - { - putenv('COLUMNS=121'); - } - - protected function tearDown(): void - { - putenv('COLUMNS'); - } - protected function getDescriptor() { return new TextDescriptor(); diff --git a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php index 2134e6275af56..c350e00c9b2af 100644 --- a/src/Symfony/Component/HttpClient/Response/StreamWrapper.php +++ b/src/Symfony/Component/HttpClient/Response/StreamWrapper.php @@ -61,7 +61,7 @@ public static function createResource(ResponseInterface $response, HttpClientInt throw new \InvalidArgumentException(sprintf('Providing a client to "%s()" is required when the response doesn\'t have any "stream()" method.', __CLASS__)); } - if (false === stream_wrapper_register('symfony', __CLASS__, \STREAM_IS_URL)) { + if (false === stream_wrapper_register('symfony', __CLASS__)) { throw new \RuntimeException(error_get_last()['message'] ?? 'Registering the "symfony" stream wrapper failed.'); } diff --git a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php index dcdde80795d9b..93e403777aa7e 100644 --- a/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php +++ b/src/Symfony/Component/HttpFoundation/Session/Storage/Handler/SessionHandlerFactory.php @@ -48,7 +48,9 @@ public static function createHandler($connection): AbstractSessionHandler case !\is_string($connection): throw new \InvalidArgumentException(sprintf('Unsupported Connection: "%s".', get_debug_type($connection))); case 0 === strpos($connection, 'file://'): - return new StrictSessionHandler(new NativeFileSessionHandler(substr($connection, 7))); + $savePath = substr($connection, 7); + + return new StrictSessionHandler(new NativeFileSessionHandler('' === $savePath ? null : $savePath)); case 0 === strpos($connection, 'redis:'): case 0 === strpos($connection, 'rediss:'): diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php index 2663fba6b4b69..a7f7e8f81751e 100644 --- a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/MemcachedSessionHandlerTest.php @@ -38,7 +38,15 @@ protected function setUp(): void $this->markTestSkipped('Tests can only be run with memcached extension 2.1.0 or lower, or 3.0.0b1 or higher'); } - $this->memcached = $this->createMock(\Memcached::class); + $r = new \ReflectionClass(\Memcached::class); + $methodsToMock = array_map(function ($m) { return $m->name; }, $r->getMethods(\ReflectionMethod::IS_PUBLIC)); + $methodsToMock = array_diff($methodsToMock, ['getDelayed','getDelayedByKey']); + + $this->memcached = $this->getMockBuilder(\Memcached::class) + ->disableOriginalConstructor() + ->setMethods($methodsToMock) + ->getMock(); + $this->storage = new MemcachedSessionHandler( $this->memcached, ['prefix' => self::PREFIX, 'expiretime' => self::TTL] diff --git a/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php new file mode 100644 index 0000000000000..46d6cd40151d5 --- /dev/null +++ b/src/Symfony/Component/HttpFoundation/Tests/Session/Storage/Handler/SessionHandlerFactoryTest.php @@ -0,0 +1,48 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\HttpFoundation\Tests\Session\Storage\Handler; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\SessionHandlerFactory; +use Symfony\Component\HttpFoundation\Session\Storage\Handler\StrictSessionHandler; + +/** + * Test class for SessionHandlerFactory. + * + * @author Simon + * + * @runTestsInSeparateProcesses + * @preserveGlobalState disabled + */ +class SessionHandlerFactoryTest extends TestCase +{ + /** + * @dataProvider provideConnectionDSN + */ + public function testCreateHandler(string $connectionDSN, string $expectedPath, string $expectedHandlerType) + { + $handler = SessionHandlerFactory::createHandler($connectionDSN); + + $this->assertInstanceOf($expectedHandlerType, $handler); + $this->assertEquals($expectedPath, ini_get('session.save_path')); + } + + public function provideConnectionDSN(): array + { + $base = sys_get_temp_dir(); + + return [ + 'native file handler using save_path from php.ini' => ['connectionDSN' => 'file://', 'expectedPath' => ini_get('session.save_path'), 'expectedHandlerType' => StrictSessionHandler::class], + 'native file handler using provided save_path' => ['connectionDSN' => 'file://'.$base.'/session/storage', 'expectedPath' => $base.'/session/storage', 'expectedHandlerType' => StrictSessionHandler::class], + ]; + } +} diff --git a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php index c9983f6114a7f..cf8682257ee8f 100644 --- a/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php +++ b/src/Symfony/Component/HttpKernel/HttpCache/ResponseCacheStrategy.php @@ -81,12 +81,15 @@ public function add(Response $response) return; } - $this->storeRelativeAgeDirective('max-age', $response->headers->getCacheControlDirective('max-age'), $age); - $this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age); + $isHeuristicallyCacheable = $response->headers->hasCacheControlDirective('public'); + $maxAge = $response->headers->hasCacheControlDirective('max-age') ? (int) $response->headers->getCacheControlDirective('max-age') : null; + $this->storeRelativeAgeDirective('max-age', $maxAge, $age, $isHeuristicallyCacheable); + $sharedMaxAge = $response->headers->hasCacheControlDirective('s-maxage') ? (int) $response->headers->getCacheControlDirective('s-maxage') : $maxAge; + $this->storeRelativeAgeDirective('s-maxage', $sharedMaxAge, $age, $isHeuristicallyCacheable); $expires = $response->getExpires(); $expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null; - $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0); + $this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0, $isHeuristicallyCacheable); } /** @@ -197,11 +200,29 @@ private function willMakeFinalResponseUncacheable(Response $response): bool * we have to subtract the age so that the value is normalized for an age of 0. * * If the value is lower than the currently stored value, we update the value, to keep a rolling - * minimal value of each instruction. If the value is NULL, the directive will not be set on the final response. + * minimal value of each instruction. + * + * If the value is NULL and the isHeuristicallyCacheable parameter is false, the directive will + * not be set on the final response. In this case, not all responses had the directive set and no + * value can be found that satisfies the requirements of all responses. The directive will be dropped + * from the final response. + * + * If the isHeuristicallyCacheable parameter is true, however, the current response has been marked + * as cacheable in a public (shared) cache, but did not provide an explicit lifetime that would serve + * as an upper bound. In this case, we can proceed and possibly keep the directive on the final response. */ - private function storeRelativeAgeDirective(string $directive, ?int $value, int $age) + private function storeRelativeAgeDirective(string $directive, ?int $value, int $age, bool $isHeuristicallyCacheable) { if (null === $value) { + if ($isHeuristicallyCacheable) { + /* + * See https://datatracker.ietf.org/doc/html/rfc7234#section-4.2.2 + * This particular response does not require maximum lifetime; heuristics might be applied. + * Other responses, however, might have more stringent requirements on maximum lifetime. + * So, return early here so that the final response can have the more limiting value set. + */ + return; + } $this->ageDirectives[$directive] = false; } diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 1793992a9f451..d251c23ec2a2c 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -75,11 +75,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private static $freshCache = []; - public const VERSION = '5.3.2'; - public const VERSION_ID = 50302; + public const VERSION = '5.3.3'; + public const VERSION_ID = 50303; public const MAJOR_VERSION = 5; public const MINOR_VERSION = 3; - public const RELEASE_VERSION = 2; + public const RELEASE_VERSION = 3; public const EXTRA_VERSION = ''; public const END_OF_MAINTENANCE = '01/2022'; diff --git a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php index 3927684eb654f..fa0ad5d311ed5 100644 --- a/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php +++ b/src/Symfony/Component/HttpKernel/Tests/HttpCache/ResponseCacheStrategyTest.php @@ -370,13 +370,53 @@ public function cacheControlMergingProvider() ]; yield 'merge max-age and s-maxage' => [ - ['public' => true, 's-maxage' => '60', 'max-age' => null], + ['public' => true, 'max-age' => '60'], ['public' => true, 's-maxage' => 3600], [ ['public' => true, 'max-age' => 60], ], ]; + yield 's-maxage may be set to 0' => [ + ['public' => true, 's-maxage' => '0', 'max-age' => null], + ['public' => true, 's-maxage' => '0'], + [ + ['public' => true, 's-maxage' => '60'], + ], + ]; + + yield 's-maxage may be set to 0, and works independently from maxage' => [ + ['public' => true, 's-maxage' => '0', 'max-age' => '30'], + ['public' => true, 's-maxage' => '0', 'max-age' => '30'], + [ + ['public' => true, 'max-age' => '60'], + ], + ]; + + yield 'public subresponse without lifetime does not remove lifetime for main response' => [ + ['public' => true, 's-maxage' => '30', 'max-age' => null], + ['public' => true, 's-maxage' => '30'], + [ + ['public' => true], + ], + ]; + + yield 'lifetime for subresponse is kept when main response has no lifetime' => [ + ['public' => true, 'max-age' => '30'], + ['public' => true], + [ + ['public' => true, 'max-age' => '30'], + ], + ]; + + yield 's-maxage on the subresponse implies public, so the result is public as well' => [ + ['public' => true, 'max-age' => '10', 's-maxage' => null], + ['public' => true, 'max-age' => '10'], + [ + ['max-age' => '30', 's-maxage' => '20'], + ], + ]; + yield 'result is private when combining private responses' => [ ['no-cache' => false, 'must-revalidate' => false, 'private' => true], ['s-maxage' => 60, 'private' => true], diff --git a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php index 72130dcee4037..956d3e269ef46 100644 --- a/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php +++ b/src/Symfony/Component/Mailer/Tests/Transport/Smtp/SmtpTransportTest.php @@ -114,6 +114,25 @@ public function testSendInvalidMessage() $this->assertNotContains("\r\n.\r\n", $stream->getCommands()); $this->assertTrue($stream->isClosed()); } + + public function testWriteEncodedRecipientAndSenderAddresses() + { + $stream = new DummyStream(); + + $transport = new SmtpTransport($stream); + + $message = new Email(); + $message->from('sender@exämple.org'); + $message->addTo('recipient@exämple.org'); + $message->addTo('recipient2@example.org'); + $message->text('.'); + + $transport->send($message); + + $this->assertContains("MAIL FROM:\r\n", $stream->getCommands()); + $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); + $this->assertContains("RCPT TO:\r\n", $stream->getCommands()); + } } class DummyStream extends AbstractStream diff --git a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php index 2e1448f39a896..ac81d81a12e27 100644 --- a/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php +++ b/src/Symfony/Component/Mailer/Transport/Smtp/SmtpTransport.php @@ -194,9 +194,9 @@ protected function doSend(SentMessage $message): void try { $envelope = $message->getEnvelope(); - $this->doMailFromCommand($envelope->getSender()->getAddress()); + $this->doMailFromCommand($envelope->getSender()->getEncodedAddress()); foreach ($envelope->getRecipients() as $recipient) { - $this->doRcptToCommand($recipient->getAddress()); + $this->doRcptToCommand($recipient->getEncodedAddress()); } $this->executeCommand("DATA\r\n", [354]); diff --git a/src/Symfony/Component/Messenger/Envelope.php b/src/Symfony/Component/Messenger/Envelope.php index aa56d1b584027..a066f5044d13c 100644 --- a/src/Symfony/Component/Messenger/Envelope.php +++ b/src/Symfony/Component/Messenger/Envelope.php @@ -127,6 +127,6 @@ private function resolveAlias(string $fqcn): string { static $resolved; - return $resolved[$fqcn] ?? ($resolved[$fqcn] = (new \ReflectionClass($fqcn))->getName()); + return $resolved[$fqcn] ?? ($resolved[$fqcn] = class_exists($fqcn) ? (new \ReflectionClass($fqcn))->getName() : $fqcn); } } diff --git a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php index 9ddcca17f30b0..6127237da74a0 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/DebugCommandTest.php @@ -29,14 +29,17 @@ */ class DebugCommandTest extends TestCase { + private $colSize; + protected function setUp(): void { + $this->colSize = getenv('COLUMNS'); putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); } protected function tearDown(): void { - putenv('COLUMNS='); + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } public function testOutput() diff --git a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php index 5afd452eba82f..53db0e0848265 100644 --- a/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php +++ b/src/Symfony/Component/Messenger/Tests/Command/FailedMessagesShowCommandTest.php @@ -29,6 +29,19 @@ */ class FailedMessagesShowCommandTest extends TestCase { + private $colSize; + + protected function setUp(): void + { + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); + } + + protected function tearDown(): void + { + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); + } + /** * @group legacy */ diff --git a/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php b/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php index 9a041f71f0763..7f03307507dd9 100644 --- a/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php +++ b/src/Symfony/Component/Messenger/Tests/EnvelopeTest.php @@ -55,6 +55,13 @@ public function testWithoutAll() $this->assertCount(1, $envelope->all(DelayStamp::class)); } + public function testWithoutAllWithNonExistentStampClass() + { + $envelope = new Envelope(new DummyMessage('dummy')); + + $this->assertInstanceOf(Envelope::class, $envelope->withoutAll(NonExistentStamp::class)); + } + public function testWithoutStampsOfType() { $envelope = new Envelope(new DummyMessage('dummy'), [ @@ -77,6 +84,13 @@ public function testWithoutStampsOfType() $this->assertEmpty($envelope5->all()); } + public function testWithoutStampsOfTypeWithNonExistentStampClass() + { + $envelope = new Envelope(new DummyMessage('dummy')); + + $this->assertInstanceOf(Envelope::class, $envelope->withoutStampsOfType(NonExistentStamp::class)); + } + public function testLast() { $receivedStamp = new ReceivedStamp('transport'); @@ -86,6 +100,13 @@ public function testLast() $this->assertNull($envelope->last(ValidationStamp::class)); } + public function testLastWithNonExistentStampClass() + { + $envelope = new Envelope(new DummyMessage('dummy')); + + $this->assertNull($envelope->last(NonExistentStamp::class)); + } + public function testAll() { $envelope = (new Envelope($dummy = new DummyMessage('dummy'))) @@ -100,6 +121,13 @@ public function testAll() $this->assertSame($validationStamp, $stamps[ValidationStamp::class][0]); } + public function testAllWithNonExistentStampClass() + { + $envelope = new Envelope(new DummyMessage('dummy')); + + $this->assertSame([], $envelope->all(NonExistentStamp::class)); + } + public function testWrapWithMessage() { $message = new \stdClass(); diff --git a/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php index 344eea7cc743f..f7d909f2c599d 100644 --- a/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php +++ b/src/Symfony/Component/Messenger/Transport/Serialization/Normalizer/FlattenExceptionNormalizer.php @@ -42,13 +42,11 @@ public function normalize($object, $format = null, array $context = []) 'file' => $object->getFile(), 'line' => $object->getLine(), 'previous' => null === $object->getPrevious() ? null : $this->normalize($object->getPrevious(), $format, $context), + 'status' => $object->getStatusCode(), 'status_text' => $object->getStatusText(), 'trace' => $object->getTrace(), 'trace_as_string' => $object->getTraceAsString(), ]; - if (null !== $status = $object->getStatusCode()) { - $normalized['status'] = $status; - } return $normalized; } @@ -70,7 +68,7 @@ public function denormalize($data, $type, $format = null, array $context = []) $object->setMessage($data['message']); $object->setCode($data['code']); - $object->setStatusCode($data['status'] ?? null); + $object->setStatusCode($data['status'] ?? 500); $object->setClass($data['class']); $object->setFile($data['file']); $object->setLine($data['line']); diff --git a/src/Symfony/Component/Notifier/Message/EmailMessage.php b/src/Symfony/Component/Notifier/Message/EmailMessage.php index 07ac6f2d10c61..13524885f91e9 100644 --- a/src/Symfony/Component/Notifier/Message/EmailMessage.php +++ b/src/Symfony/Component/Notifier/Message/EmailMessage.php @@ -37,7 +37,7 @@ public function __construct(RawMessage $message, Envelope $envelope = null) public static function fromNotification(Notification $notification, EmailRecipientInterface $recipient): self { if ('' === $recipient->getEmail()) { - throw new InvalidArgumentException(sprintf('"%s" needs an email, it cannot be empty.', static::class)); + throw new InvalidArgumentException(sprintf('"%s" needs an email, it cannot be empty.', __CLASS__)); } if (!class_exists(NotificationEmail::class)) { diff --git a/src/Symfony/Component/Notifier/Message/SmsMessage.php b/src/Symfony/Component/Notifier/Message/SmsMessage.php index 717c160cd5ded..872dab26e34d3 100644 --- a/src/Symfony/Component/Notifier/Message/SmsMessage.php +++ b/src/Symfony/Component/Notifier/Message/SmsMessage.php @@ -27,7 +27,7 @@ final class SmsMessage implements MessageInterface public function __construct(string $phone, string $subject) { if ('' === $phone) { - throw new InvalidArgumentException(sprintf('"%s" needs a phone number, it cannot be empty.', static::class)); + throw new InvalidArgumentException(sprintf('"%s" needs a phone number, it cannot be empty.', __CLASS__)); } $this->subject = $subject; diff --git a/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php b/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php index f26164d7e51af..4ec8a1b6ab248 100644 --- a/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php +++ b/src/Symfony/Component/PasswordHasher/Hasher/UserPasswordHasher.php @@ -43,9 +43,16 @@ public function hashPassword($user, string $plainPassword): string trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user)); } - $salt = $user->getSalt(); - if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) { - trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + $salt = null; + + if ($user instanceof LegacyPasswordAuthenticatedUserInterface) { + $salt = $user->getSalt(); + } elseif ($user instanceof UserInterface) { + $salt = $user->getSalt(); + + if (null !== $salt) { + trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } } $hasher = $this->hasherFactory->getPasswordHasher($user); @@ -65,9 +72,16 @@ public function isPasswordValid($user, string $plainPassword): bool trigger_deprecation('symfony/password-hasher', '5.3', 'The "%s()" method expects a "%s" instance as first argument. Not implementing it in class "%s" is deprecated.', __METHOD__, PasswordAuthenticatedUserInterface::class, get_debug_type($user)); } - $salt = $user->getSalt(); - if ($salt && !$user instanceof LegacyPasswordAuthenticatedUserInterface) { - trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + $salt = null; + + if ($user instanceof LegacyPasswordAuthenticatedUserInterface) { + $salt = $user->getSalt(); + } elseif ($user instanceof UserInterface) { + $salt = $user->getSalt(); + + if (null !== $salt) { + trigger_deprecation('symfony/password-hasher', '5.3', 'Returning a string from "getSalt()" without implementing the "%s" interface is deprecated, the "%s" class should implement it.', LegacyPasswordAuthenticatedUserInterface::class, get_debug_type($user)); + } } if (null === $user->getPassword()) { diff --git a/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php b/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php index 42386bffecf8d..5c24ce9539e45 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Command/UserPasswordHashCommandTest.php @@ -25,6 +25,7 @@ class UserPasswordHashCommandTest extends TestCase { /** @var CommandTester */ private $passwordHasherCommandTester; + private $colSize; public function testEncodePasswordEmptySalt() { @@ -287,7 +288,9 @@ public function testThrowsExceptionOnNoConfiguredHashers() protected function setUp(): void { + $this->colSize = getenv('COLUMNS'); putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); + $hasherFactory = new PasswordHasherFactory([ InMemoryUser::class => ['algorithm' => 'plaintext'], 'Custom\Class\Native\User' => ['algorithm' => 'native', 'cost' => 10], @@ -304,12 +307,11 @@ protected function setUp(): void protected function tearDown(): void { $this->passwordHasherCommandTester = null; + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } private function setupArgon2i() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); - $hasherFactory = new PasswordHasherFactory([ 'Custom\Class\Argon2i\User' => ['algorithm' => 'argon2i'], ]); @@ -321,8 +323,6 @@ private function setupArgon2i() private function setupArgon2id() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); - $hasherFactory = new PasswordHasherFactory([ 'Custom\Class\Argon2id\User' => ['algorithm' => 'argon2id'], ]); @@ -334,8 +334,6 @@ private function setupArgon2id() private function setupBcrypt() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); - $hasherFactory = new PasswordHasherFactory([ 'Custom\Class\Bcrypt\User' => ['algorithm' => 'bcrypt'], ]); @@ -348,8 +346,6 @@ private function setupBcrypt() private function setupSodium() { - putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); - $hasherFactory = new PasswordHasherFactory([ 'Custom\Class\Sodium\User' => ['algorithm' => 'sodium'], ]); diff --git a/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestLegacyPasswordAuthenticatedUser.php b/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestLegacyPasswordAuthenticatedUser.php new file mode 100644 index 0000000000000..b0d0949c5e4ec --- /dev/null +++ b/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestLegacyPasswordAuthenticatedUser.php @@ -0,0 +1,53 @@ +roles = $roles; + $this->salt = $salt; + $this->password = $password; + $this->username = $username; + } + + public function getSalt(): ?string + { + return $this->salt; + } + + public function getPassword(): ?string + { + return $this->password; + } + + public function getRoles() + { + return $this->roles; + } + + public function eraseCredentials() + { + // Do nothing + return; + } + + public function getUsername() + { + return $this->username; + } + + public function getUserIdentifier() + { + return $this->username; + } +} diff --git a/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestPasswordAuthenticatedUser.php b/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestPasswordAuthenticatedUser.php new file mode 100644 index 0000000000000..a732ebb077751 --- /dev/null +++ b/src/Symfony/Component/PasswordHasher/Tests/Fixtures/TestPasswordAuthenticatedUser.php @@ -0,0 +1,20 @@ +password = $password; + } + + public function getPassword(): ?string + { + return $this->password; + } +} diff --git a/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php b/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php index fb9188083eab6..b483864d22d53 100644 --- a/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php +++ b/src/Symfony/Component/PasswordHasher/Tests/Hasher/UserPasswordHasherTest.php @@ -17,10 +17,10 @@ use Symfony\Component\PasswordHasher\Hasher\PasswordHasherFactoryInterface; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasher; use Symfony\Component\PasswordHasher\PasswordHasherInterface; +use Symfony\Component\PasswordHasher\Tests\Fixtures\TestLegacyPasswordAuthenticatedUser; +use Symfony\Component\PasswordHasher\Tests\Fixtures\TestPasswordAuthenticatedUser; use Symfony\Component\Security\Core\User\InMemoryUser; -use Symfony\Component\Security\Core\User\LegacyPasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\User; -use Symfony\Component\Security\Core\User\UserInterface; class UserPasswordHasherTest extends TestCase { @@ -56,12 +56,9 @@ public function testHashWithNonPasswordAuthenticatedUser() $this->assertEquals('hash', $encoded); } - public function testHash() + public function testHashWithLegacyUser() { - $userMock = $this->createMock(TestPasswordAuthenticatedUser::class); - $userMock->expects($this->any()) - ->method('getSalt') - ->willReturn('userSalt'); + $user = new TestLegacyPasswordAuthenticatedUser('name', null, 'userSalt'); $mockHasher = $this->createMock(PasswordHasherInterface::class); $mockHasher->expects($this->any()) @@ -72,25 +69,42 @@ public function testHash() $mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class); $mockPasswordHasherFactory->expects($this->any()) ->method('getPasswordHasher') - ->with($this->equalTo($userMock)) + ->with($user) ->willReturn($mockHasher); $passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory); - $encoded = $passwordHasher->hashPassword($userMock, 'plainPassword'); + $encoded = $passwordHasher->hashPassword($user, 'plainPassword'); $this->assertEquals('hash', $encoded); } - public function testVerify() + public function testHashWithPasswordAuthenticatedUser() { - $userMock = $this->createMock(TestPasswordAuthenticatedUser::class); - $userMock->expects($this->any()) - ->method('getSalt') - ->willReturn('userSalt'); - $userMock->expects($this->any()) - ->method('getPassword') + $user = new TestPasswordAuthenticatedUser(); + + $mockHasher = $this->createMock(PasswordHasherInterface::class); + $mockHasher->expects($this->any()) + ->method('hash') + ->with($this->equalTo('plainPassword'), $this->equalTo(null)) ->willReturn('hash'); + $mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class); + $mockPasswordHasherFactory->expects($this->any()) + ->method('getPasswordHasher') + ->with($user) + ->willReturn($mockHasher); + + $passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory); + + $hashedPassword = $passwordHasher->hashPassword($user, 'plainPassword'); + + $this->assertSame('hash', $hashedPassword); + } + + public function testVerifyWithLegacyUser() + { + $user = new TestLegacyPasswordAuthenticatedUser('user', 'hash', 'userSalt'); + $mockHasher = $this->createMock(PasswordHasherInterface::class); $mockHasher->expects($this->any()) ->method('verify') @@ -100,12 +114,34 @@ public function testVerify() $mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class); $mockPasswordHasherFactory->expects($this->any()) ->method('getPasswordHasher') - ->with($this->equalTo($userMock)) + ->with($user) + ->willReturn($mockHasher); + + $passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory); + + $isValid = $passwordHasher->isPasswordValid($user, 'plainPassword'); + $this->assertTrue($isValid); + } + + public function testVerify() + { + $user = new TestPasswordAuthenticatedUser('hash'); + + $mockHasher = $this->createMock(PasswordHasherInterface::class); + $mockHasher->expects($this->any()) + ->method('verify') + ->with($this->equalTo('hash'), $this->equalTo('plainPassword'), $this->equalTo(null)) + ->willReturn(true); + + $mockPasswordHasherFactory = $this->createMock(PasswordHasherFactoryInterface::class); + $mockPasswordHasherFactory->expects($this->any()) + ->method('getPasswordHasher') + ->with($user) ->willReturn($mockHasher); $passwordHasher = new UserPasswordHasher($mockPasswordHasherFactory); - $isValid = $passwordHasher->isPasswordValid($userMock, 'plainPassword'); + $isValid = $passwordHasher->isPasswordValid($user, 'plainPassword'); $this->assertTrue($isValid); } @@ -128,7 +164,3 @@ public function testNeedsRehash() $this->assertFalse($passwordHasher->needsRehash($user)); } } - -abstract class TestPasswordAuthenticatedUser implements LegacyPasswordAuthenticatedUserInterface, UserInterface -{ -} diff --git a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php index fc6e5a0c99166..77c48b3f0b380 100644 --- a/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php +++ b/src/Symfony/Component/Runtime/Internal/ComposerPlugin.php @@ -93,7 +93,7 @@ public function updateAutoloadFile(): void if (!$nestingLevel) { $projectDir = '__'.'DIR__.'.var_export('/'.$projectDir, true); } else { - $projectDir = 'dirname(__'."DIR__, $nestingLevel)".('' !== $projectDir ? var_export('/'.$projectDir, true) : ''); + $projectDir = 'dirname(__'."DIR__, $nestingLevel)".('' !== $projectDir ? '.'.var_export('/'.$projectDir, true) : ''); } $runtimeClass = $extra['class'] ?? SymfonyRuntime::class; diff --git a/src/Symfony/Component/Security/Core/Authentication/RememberMe/CacheTokenVerifier.php b/src/Symfony/Component/Security/Core/Authentication/RememberMe/CacheTokenVerifier.php index 1f4241e6a7712..2b96eff06287a 100644 --- a/src/Symfony/Component/Security/Core/Authentication/RememberMe/CacheTokenVerifier.php +++ b/src/Symfony/Component/Security/Core/Authentication/RememberMe/CacheTokenVerifier.php @@ -43,11 +43,12 @@ public function verifyToken(PersistentTokenInterface $token, string $tokenValue) return true; } - if (!$this->cache->hasItem($this->cacheKeyPrefix.$token->getSeries())) { + $cacheKey = $this->getCacheKey($token); + if (!$this->cache->hasItem($cacheKey)) { return false; } - $item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries()); + $item = $this->cache->getItem($cacheKey); $outdatedToken = $item->get(); return hash_equals($outdatedToken, $tokenValue); @@ -60,9 +61,14 @@ public function updateExistingToken(PersistentTokenInterface $token, string $tok { // When a token gets updated, persist the outdated token for $outdatedTokenTtl seconds so we can // still accept it as valid in verifyToken - $item = $this->cache->getItem($this->cacheKeyPrefix.$token->getSeries()); + $item = $this->cache->getItem($this->getCacheKey($token)); $item->set($token->getTokenValue()); $item->expiresAfter($this->outdatedTokenTtl); $this->cache->save($item); } + + private function getCacheKey(PersistentTokenInterface $token): string + { + return $this->cacheKeyPrefix.rawurlencode($token->getSeries()); + } } diff --git a/src/Symfony/Component/Security/Core/Exception/UserNotFoundException.php b/src/Symfony/Component/Security/Core/Exception/UserNotFoundException.php index 685b079ef17e8..4f8b7ef77b192 100644 --- a/src/Symfony/Component/Security/Core/Exception/UserNotFoundException.php +++ b/src/Symfony/Component/Security/Core/Exception/UserNotFoundException.php @@ -32,7 +32,7 @@ public function getMessageKey() /** * Get the user identifier (e.g. username or e-mailaddress). */ - public function getUserIdentifier(): string + public function getUserIdentifier(): ?string { return $this->identifier; } diff --git a/src/Symfony/Component/Security/Core/Resources/translations/security.hy.xlf b/src/Symfony/Component/Security/Core/Resources/translations/security.hy.xlf index da63a0047c664..459c292be31a6 100644 --- a/src/Symfony/Component/Security/Core/Resources/translations/security.hy.xlf +++ b/src/Symfony/Component/Security/Core/Resources/translations/security.hy.xlf @@ -36,7 +36,7 @@ No session available, it either timed out or cookies are not enabled. - No session available, it either timed out or cookies are not enabled. + Հասանելի սեսիա չկա, կամ այն սպառվել է կամ cookie-ները անջատված են: No token could be found. diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php index 709ad2834a9cc..996a42e4a6abf 100644 --- a/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/RememberMe/CacheTokenVerifierTest.php @@ -21,22 +21,22 @@ class CacheTokenVerifierTest extends TestCase public function testVerifyCurrentToken() { $verifier = new CacheTokenVerifier(new ArrayAdapter()); - $token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); + $token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime()); $this->assertTrue($verifier->verifyToken($token, 'value')); } public function testVerifyFailsInvalidToken() { $verifier = new CacheTokenVerifier(new ArrayAdapter()); - $token = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); + $token = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime()); $this->assertFalse($verifier->verifyToken($token, 'wrong-value')); } public function testVerifyOutdatedToken() { $verifier = new CacheTokenVerifier(new ArrayAdapter()); - $outdatedToken = new PersistentToken('class', 'user', 'series1', 'value', new \DateTime()); - $newToken = new PersistentToken('class', 'user', 'series1', 'newvalue', new \DateTime()); + $outdatedToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'value', new \DateTime()); + $newToken = new PersistentToken('class', 'user', 'series1@special:chars=/', 'newvalue', new \DateTime()); $verifier->updateExistingToken($outdatedToken, 'newvalue', new \DateTime()); $this->assertTrue($verifier->verifyToken($newToken, 'value')); } diff --git a/src/Symfony/Component/Security/Core/Tests/Exception/UserNotFoundExceptionTest.php b/src/Symfony/Component/Security/Core/Tests/Exception/UserNotFoundExceptionTest.php index 559e62acd97d0..3d9de8f14a2ef 100644 --- a/src/Symfony/Component/Security/Core/Tests/Exception/UserNotFoundExceptionTest.php +++ b/src/Symfony/Component/Security/Core/Tests/Exception/UserNotFoundExceptionTest.php @@ -12,8 +12,8 @@ namespace Symfony\Component\Security\Core\Tests\Exception; use PHPUnit\Framework\TestCase; -use Symfony\Component\Security\Core\Exception\UserNotFoundException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Security\Core\Exception\UserNotFoundException; class UserNotFoundExceptionTest extends TestCase { @@ -25,6 +25,23 @@ public function testGetMessageData() $this->assertEquals(['{{ username }}' => 'username', '{{ user_identifier }}' => 'username'], $exception->getMessageData()); } + public function testUserIdentifierIsNotSetByDefault() + { + $exception = new UserNotFoundException(); + + $this->assertNull($exception->getUserIdentifier()); + } + + /** + * @group legacy + */ + public function testUsernameIsNotSetByDefault() + { + $exception = new UserNotFoundException(); + + $this->assertNull($exception->getUsername()); + } + /** * @group legacy */ diff --git a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php index 230f33fb257f3..6c9bf9820b0c7 100644 --- a/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php +++ b/src/Symfony/Component/Security/Csrf/Tests/TokenStorage/SessionTokenStorageTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\Security\Csrf\Tests\TokenStorage; use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpFoundation\Session\Session; @@ -24,6 +25,8 @@ */ class SessionTokenStorageTest extends TestCase { + use ExpectDeprecationTrait; + private const SESSION_NAMESPACE = 'foobar'; /** @@ -159,4 +162,50 @@ public function testClearDoesNotRemoveNonNamespacedSessionValues() $this->assertTrue($this->session->has('foo')); $this->assertSame('baz', $this->session->get('foo')); } + + /** + * @group legacy + */ + public function testMockSessionIsCreatedWhenMissing() + { + $this->expectDeprecation('Since symfony/security-csrf 5.3: Using the "Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage" without a session has no effect and is deprecated. It will throw a "Symfony\Component\HttpFoundation\Exception\SessionNotFoundException" in Symfony 6.0'); + + $this->storage->setToken('token_id', 'TOKEN'); + + $requestStack = new RequestStack(); + $storage = new SessionTokenStorage($requestStack, self::SESSION_NAMESPACE); + + $this->assertFalse($storage->hasToken('foo')); + $storage->setToken('foo', 'bar'); + $this->assertTrue($storage->hasToken('foo')); + $this->assertSame('bar', $storage->getToken('foo')); + + $session = new Session(new MockArraySessionStorage()); + $request = new Request(); + $request->setSession($session); + $requestStack->push($request); + } + + /** + * @group legacy + */ + public function testMockSessionIsReusedEvenWhenRequestHasSession() + { + $this->expectDeprecation('Since symfony/security-csrf 5.3: Using the "Symfony\Component\Security\Csrf\TokenStorage\SessionTokenStorage" without a session has no effect and is deprecated. It will throw a "Symfony\Component\HttpFoundation\Exception\SessionNotFoundException" in Symfony 6.0'); + + $this->storage->setToken('token_id', 'TOKEN'); + + $requestStack = new RequestStack(); + $storage = new SessionTokenStorage($requestStack, self::SESSION_NAMESPACE); + + $storage->setToken('foo', 'bar'); + $this->assertSame('bar', $storage->getToken('foo')); + + $session = new Session(new MockArraySessionStorage()); + $request = new Request(); + $request->setSession($session); + $requestStack->push($request); + + $this->assertSame('bar', $storage->getToken('foo')); + } } diff --git a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php index 70613f5f26f25..5b86499bc9e8a 100644 --- a/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php +++ b/src/Symfony/Component/Security/Csrf/TokenStorage/SessionTokenStorage.php @@ -34,7 +34,7 @@ class SessionTokenStorage implements ClearableTokenStorageInterface private $requestStack; private $namespace; /** - * Tp be remove in Symfony 6.0 + * To be removed in Symfony 6.0. */ private $session; @@ -130,7 +130,7 @@ public function clear() private function getSession(): SessionInterface { try { - return $this->requestStack->getSession(); + return $this->session ?? $this->requestStack->getSession(); } catch (SessionNotFoundException $e) { trigger_deprecation('symfony/security-csrf', '5.3', 'Using the "%s" without a session has no effect and is deprecated. It will throw a "%s" in Symfony 6.0', __CLASS__, SessionNotFoundException::class); diff --git a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php index 876677851fa69..d961ef609495e 100644 --- a/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php +++ b/src/Symfony/Component/Security/Http/Authenticator/Passport/Badge/RememberMeBadge.php @@ -11,12 +11,14 @@ namespace Symfony\Component\Security\Http\Authenticator\Passport\Badge; +use Symfony\Component\Security\Http\EventListener\CheckRememberMeConditionsListener; + /** * Adds support for remember me to this authenticator. * * The presence of this badge doesn't create the remember-me cookie. The actual * cookie is only created if this badge is enabled. By default, this is done - * by the {@see RememberMeConditionsListener} if all conditions are met. + * by the {@see CheckRememberMeConditionsListener} if all conditions are met. * * @author Wouter de Jong * @@ -29,7 +31,7 @@ class RememberMeBadge implements BadgeInterface /** * Enables remember-me cookie creation. * - * In most cases, {@see RememberMeConditionsListener} enables this + * In most cases, {@see CheckRememberMeConditionsListener} enables this * automatically if always_remember_me is true or the remember_me_parameter * exists in the request. * @@ -47,10 +49,14 @@ public function enable(): self * * The default is disabled, this can be called to suppress creation * after it was enabled. + * + * @return $this */ - public function disable(): void + public function disable(): self { $this->enabled = false; + + return $this; } public function isEnabled(): bool diff --git a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php index 2be8cbc0becff..f60bd9d6b9141 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php +++ b/src/Symfony/Component/Security/Http/RememberMe/PersistentRememberMeHandler.php @@ -98,7 +98,7 @@ public function processRememberMe(RememberMeDetails $rememberMeDetails, UserInte $this->tokenProvider->updateToken($series, $tokenValueHash, $tokenLastUsed); } - $this->createCookie($rememberMeDetails->withValue($tokenValue)); + $this->createCookie($rememberMeDetails->withValue($series.':'.$tokenValue)); } /** diff --git a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php index 2e1e202808c4b..ba9b118a34af7 100644 --- a/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php +++ b/src/Symfony/Component/Security/Http/RememberMe/RememberMeDetails.php @@ -40,6 +40,9 @@ public static function fromRawCookie(string $rawCookie): self if (false === $cookieParts[1] = base64_decode($cookieParts[1], true)) { throw new AuthenticationException('The user identifier contains a character from outside the base64 alphabet.'); } + if (4 !== \count($cookieParts)) { + throw new AuthenticationException('The cookie contains invalid data.'); + } return new static(...$cookieParts); } diff --git a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php index 27adff550d784..b0b295d70db36 100644 --- a/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php +++ b/src/Symfony/Component/Security/Http/Tests/Authenticator/RememberMeAuthenticatorTest.php @@ -16,6 +16,7 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\InMemoryUser; use Symfony\Component\Security\Http\Authenticator\RememberMeAuthenticator; use Symfony\Component\Security\Http\RememberMe\RememberMeDetails; @@ -80,4 +81,12 @@ public function testAuthenticateWithoutToken() $this->authenticator->authenticate(Request::create('/')); } + + public function testAuthenticateWithoutOldToken() + { + $this->expectException(AuthenticationException::class); + + $request = Request::create('/', 'GET', [], ['_remember_me_cookie' => base64_encode('foo:bar')]); + $this->authenticator->authenticate($request); + } } diff --git a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php index 44779829c613f..00ce37b8dac6e 100644 --- a/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php +++ b/src/Symfony/Component/Security/Http/Tests/RememberMe/PersistentRememberMeHandlerTest.php @@ -92,8 +92,14 @@ public function testConsumeRememberMeCookieValid() /** @var Cookie $cookie */ $cookie = $this->request->attributes->get(ResponseListener::COOKIE_ATTR_NAME); - $this->assertNotEquals($rememberMeDetails->toString(), $cookie->getValue()); - $this->assertMatchesRegularExpression('{'.str_replace('\\', '\\\\', base64_decode($rememberMeDetails->withValue('[a-zA-Z0-9/+]+')->toString())).'}', base64_decode($cookie->getValue())); + $rememberParts = explode(':', base64_decode($rememberMeDetails->toString()), 4); + $cookieParts = explode(':', base64_decode($cookie->getValue()), 4); + + $this->assertSame($rememberParts[0], $cookieParts[0]); // class + $this->assertSame($rememberParts[1], $cookieParts[1]); // identifier + $this->assertSame($rememberParts[2], $cookieParts[2]); // expire + $this->assertNotSame($rememberParts[3], $cookieParts[3]); // value + $this->assertSame(explode(':', $rememberParts[3])[0], explode(':', $cookieParts[3])[0]); // series } public function testConsumeRememberMeCookieInvalidToken() diff --git a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php index d9598d73637c3..70e4ba311d7a6 100644 --- a/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractAsciiTestCase.php @@ -36,6 +36,8 @@ public function testCreateFromEmptyString() /** * @dataProvider provideBytesAt + * + * @requires extension intl 66.2 */ public function testBytesAt(array $expected, string $string, int $offset, int $form = null) { @@ -157,6 +159,8 @@ public static function provideWrap(): array /** * @dataProvider provideLength + * + * @requires extension intl 66.2 */ public function testLength(int $length, string $string) { diff --git a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php index 5617bc051e14c..0f2a58404c41a 100644 --- a/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php +++ b/src/Symfony/Component/String/Tests/AbstractUnicodeTestCase.php @@ -57,6 +57,8 @@ public static function provideBytesAt(): array /** * @dataProvider provideCodePointsAt + * + * @requires extension intl 66.2 */ public function testCodePointsAt(array $expected, string $string, int $offset, int $form = null) { diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php index 3354bba6300f6..73b562dec76ff 100644 --- a/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPullCommandTest.php @@ -27,16 +27,19 @@ */ class TranslationPullCommandTest extends TranslationProviderTestCase { + private $colSize; + protected function setUp(): void { - putenv('COLUMNS=121'); + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); parent::setUp(); } protected function tearDown(): void { parent::tearDown(); - putenv('COLUMNS'); + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } public function testPullNewXlf12Messages() diff --git a/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php index 9b963372da879..1f0db90d3fbff 100644 --- a/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php +++ b/src/Symfony/Component/Translation/Tests/Command/TranslationPushCommandTest.php @@ -26,16 +26,19 @@ */ class TranslationPushCommandTest extends TranslationProviderTestCase { + private $colSize; + protected function setUp(): void { - putenv('COLUMNS=121'); + $this->colSize = getenv('COLUMNS'); + putenv('COLUMNS='.(119 + \strlen(\PHP_EOL))); parent::setUp(); } protected function tearDown(): void { parent::tearDown(); - putenv('COLUMNS'); + putenv($this->colSize ? 'COLUMNS='.$this->colSize : 'COLUMNS'); } public function testPushNewMessages() diff --git a/src/Symfony/Component/Uid/AbstractUid.php b/src/Symfony/Component/Uid/AbstractUid.php index 66dcd9f5e8cae..ddcd604f9682e 100644 --- a/src/Symfony/Component/Uid/AbstractUid.php +++ b/src/Symfony/Component/Uid/AbstractUid.php @@ -97,7 +97,7 @@ public static function fromRfc4122(string $uid): self abstract public function toBinary(): string; /** - * Returns the identifier as a base-58 case sensitive string. + * Returns the identifier as a base58 case sensitive string. */ public function toBase58(): string { @@ -105,7 +105,7 @@ public function toBase58(): string } /** - * Returns the identifier as a base-32 case insensitive string. + * Returns the identifier as a base32 case insensitive string. */ public function toBase32(): string { diff --git a/src/Symfony/Component/Uid/Tests/UlidTest.php b/src/Symfony/Component/Uid/Tests/UlidTest.php index 3a5d58926f377..8c27e692214ce 100644 --- a/src/Symfony/Component/Uid/Tests/UlidTest.php +++ b/src/Symfony/Component/Uid/Tests/UlidTest.php @@ -237,4 +237,9 @@ public function testFromStringOnExtendedClassReturnsStatic() { $this->assertInstanceOf(CustomUlid::class, CustomUlid::fromString((new CustomUlid())->toBinary())); } + + public function testFromStringBase58Padding() + { + $this->assertInstanceOf(Ulid::class, Ulid::fromString('111111111u9QRyVM94rdmZ')); + } } diff --git a/src/Symfony/Component/Uid/Tests/UuidTest.php b/src/Symfony/Component/Uid/Tests/UuidTest.php index 2903eda771a03..ba9df678985d3 100644 --- a/src/Symfony/Component/Uid/Tests/UuidTest.php +++ b/src/Symfony/Component/Uid/Tests/UuidTest.php @@ -187,14 +187,24 @@ public function testCompare() $this->assertSame([$a, $b, $c, $d], $uuids); } - public function testNilUuid() + /** + * @testWith ["00000000-0000-0000-0000-000000000000"] + * ["1111111111111111111111"] + * ["00000000000000000000000000"] + */ + public function testNilUuid(string $uuid) { - $uuid = Uuid::fromString('00000000-0000-0000-0000-000000000000'); + $uuid = Uuid::fromString($uuid); $this->assertInstanceOf(NilUuid::class, $uuid); $this->assertSame('00000000-0000-0000-0000-000000000000', (string) $uuid); } + public function testNewNilUuid() + { + $this->assertSame('00000000-0000-0000-0000-000000000000', (string) new NilUuid()); + } + public function testFromBinary() { $this->assertEquals( @@ -318,4 +328,9 @@ public function testGetDateTime() $this->assertEquals(\DateTimeImmutable::createFromFormat('U.u', '-0.000001'), ((new UuidV1('13813ff6-1dd2-11b2-a456-426655440000'))->getDateTime())); $this->assertEquals(new \DateTimeImmutable('@-12219292800'), ((new UuidV1('00000000-0000-1000-a456-426655440000'))->getDateTime())); } + + public function testFromStringBase58Padding() + { + $this->assertInstanceOf(Uuid::class, Uuid::fromString('111111111u9QRyVM94rdmZ')); + } } diff --git a/src/Symfony/Component/Uid/Ulid.php b/src/Symfony/Component/Uid/Ulid.php index d74ff6fb2aa6f..21a7b3b3b56b0 100644 --- a/src/Symfony/Component/Uid/Ulid.php +++ b/src/Symfony/Component/Uid/Ulid.php @@ -43,7 +43,7 @@ public function __construct(string $ulid = null) throw new \InvalidArgumentException(sprintf('Invalid ULID: "%s".', $ulid)); } - $this->uid = strtr($ulid, 'abcdefghjkmnpqrstvwxyz', 'ABCDEFGHJKMNPQRSTVWXYZ'); + $this->uid = strtoupper($ulid); } public static function isValid(string $ulid): bool @@ -67,7 +67,7 @@ public static function fromString(string $ulid): parent if (36 === \strlen($ulid) && Uuid::isValid($ulid)) { $ulid = (new Uuid($ulid))->toBinary(); } elseif (22 === \strlen($ulid) && 22 === strspn($ulid, BinaryUtil::BASE58[''])) { - $ulid = BinaryUtil::fromBase($ulid, BinaryUtil::BASE58); + $ulid = str_pad(BinaryUtil::fromBase($ulid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); } if (16 !== \strlen($ulid)) { diff --git a/src/Symfony/Component/Uid/Uuid.php b/src/Symfony/Component/Uid/Uuid.php index dc666d28e31ab..58c2871c49665 100644 --- a/src/Symfony/Component/Uid/Uuid.php +++ b/src/Symfony/Component/Uid/Uuid.php @@ -43,7 +43,7 @@ public function __construct(string $uuid) public static function fromString(string $uuid): parent { if (22 === \strlen($uuid) && 22 === strspn($uuid, BinaryUtil::BASE58[''])) { - $uuid = BinaryUtil::fromBase($uuid, BinaryUtil::BASE58); + $uuid = str_pad(BinaryUtil::fromBase($uuid, BinaryUtil::BASE58), 16, "\0", \STR_PAD_LEFT); } if (16 === \strlen($uuid)) { @@ -54,7 +54,9 @@ public static function fromString(string $uuid): parent $uuid = substr_replace($uuid, '-', 18, 0); $uuid = substr_replace($uuid, '-', 23, 0); } elseif (26 === \strlen($uuid) && Ulid::isValid($uuid)) { - $uuid = (new Ulid($uuid))->toRfc4122(); + $ulid = new Ulid('00000000000000000000000000'); + $ulid->uid = strtoupper($uuid); + $uuid = $ulid->toRfc4122(); } if (__CLASS__ !== static::class || 36 !== \strlen($uuid)) { diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf index fa87a3753de67..c6a38c57dab7e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ar.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). صالح (ISIN) هذه القيمة ليست رقم تعريف الأوراق المالية الدولي. + + This value should be a valid expression. + يجب أن تكون هذه القيمة تعبيرًا صالحًا. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf index 0cf53015addb6..4d990e4d49358 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.cs.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Tato hodnota není platné mezinárodní identifikační číslo cenného papíru (ISIN). + + This value should be a valid expression. + Tato hodnota musí být platný výraz. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf index ecc73e48aa1ef..84cd9b9dcd9c2 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.en.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). This value is not a valid International Securities Identification Number (ISIN). + + This value should be a valid expression. + This value should be a valid expression. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf index 2c586ca4a2571..c73138b0ee277 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.es.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Este valor no es un número de identificación internacional de valores (ISIN) válido. + + This value should be a valid expression. + Este valor debería ser una expresión válida. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf index a4dd54295b46a..c61ff92c6d473 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.fr.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Cette valeur n'est pas un code international de sécurité valide (ISIN). + + This value should be a valid expression. + Cette valeur doit être une expression valide. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf index acd69a1009c13..a3264d5543af4 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.hu.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Ez az érték nem egy érvényes nemzetközi értékpapír-azonosító szám (ISIN). + + This value should be a valid expression. + Ennek az értéknek érvényes kifejezésnek kell lennie. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf index 1af8185e80e16..bca112204ddc8 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.it.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Questo valore non è un codice identificativo internazionale di valori mobiliari (ISIN) valido. + + This value should be a valid expression. + Questo valore dovrebbe essere un'espressione valida. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf index fef436539f296..eeb0727349573 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.lt.xlf @@ -131,25 +131,25 @@ Ši reikšmė turi būti skaičius. - This value is not a valid country. - Ši reikšmė nėra tinkama šalis. - - This file is not a valid image. Byla nėra paveikslėlis. - + This is not a valid IP address. Ši reikšmė nėra tinkamas IP adresas. - + This value is not a valid language. Ši reikšmė nėra tinkama kalba. - + This value is not a valid locale. Ši reikšmė nėra tinkama lokalė. + + This value is not a valid country. + Ši reikšmė nėra tinkama šalis. + This value is already used. Ši reikšmė jau yra naudojama. @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Ši reišmė neatitinka tarptautinio vertybinių popierių identifikavimo numerio formato (ISIN). + + This value should be a valid expression. + Ši vertė turėtų būti teisinga išraiška. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf index 561a7d40500b5..0881f3167293a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pl.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Ta wartość nie jest prawidłowym Międzynarodowym Numerem Identyfikacyjnym Papierów Wartościowych (ISIN). + + This value should be a valid expression. + Ta wartość powinna być prawidłowym wyrażeniem. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf index 71bdaf8bc19f1..5caa804dd1712 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Este valor não é um Número Internacional de Identificação de Segurança (ISIN) válido. + + This value should be a valid expression. + Este valor deve ser uma expressão válida. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf index 2dbd009ccdd53..c6297ca90157a 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.pt_BR.xlf @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Este valor não é um Número de Identificação de Títulos Internacionais (ISIN) válido. + + This value should be a valid expression. + Este valor deve ser uma expressão válida. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf index 516fa2cf2a8bb..2c7a0444ef51e 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.ru.xlf @@ -131,25 +131,25 @@ Значение должно быть числом. - This value is not a valid country. - Значение не является допустимой страной. - - This file is not a valid image. Файл не является допустимым форматом изображения. - + This is not a valid IP address. Значение не является допустимым IP адресом. - + This value is not a valid language. Значение не является допустимым языком. - + This value is not a valid locale. Значение не является допустимой локалью. + + This value is not a valid country. + Значение не является допустимой страной. + This value is already used. Это значение уже используется. @@ -386,6 +386,10 @@ This value is not a valid International Securities Identification Number (ISIN). Значение не является корректным международным идентификационным номером ценных бумаг (ISIN). + + This value should be a valid expression. + Это значение должно быть корректным выражением. + diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf index 43ac9143bb963..3b5a16bd5fcd5 100644 --- a/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf +++ b/src/Symfony/Component/Validator/Resources/translations/validators.zh_CN.xlf @@ -368,7 +368,7 @@ This value is not a valid hostname. - 该数值不是有效的主机名称。 + 该值不是有效的主机名称。 The number of elements in this collection should be a multiple of {{ compared_value }}. @@ -376,7 +376,7 @@ This value should satisfy at least one of the following constraints: - 该数值需符合以下其中一个约束: + 该值需符合以下其中一个约束: Each element of this collection should satisfy its own set of constraints. @@ -384,7 +384,11 @@ This value is not a valid International Securities Identification Number (ISIN). - 该数值不是有效的国际证券识别码 (ISIN)。 + 该值不是有效的国际证券识别码 (ISIN)。 + + + This value should be a valid expression. + 该值需为一个有效的表达式。 diff --git a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php index 2987044f882a7..5aa5b37933b1f 100644 --- a/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php +++ b/src/Symfony/Component/VarDumper/Tests/Caster/ReflectionCasterTest.php @@ -138,8 +138,9 @@ public function testReflectionParameter() { $var = new \ReflectionParameter(reflectionParameterFixture::class, 0); - $this->assertDumpMatchesFormat( - <<<'EOTXT' + if (\PHP_VERSION_ID < 80100) { + $this->assertDumpMatchesFormat( + <<<'EOTXT' ReflectionParameter { +name: "arg1" position: 0 @@ -147,8 +148,21 @@ public function testReflectionParameter() default: null } EOTXT - , $var - ); + , $var + ); + } else { + $this->assertDumpMatchesFormat( + <<<'EOTXT' +ReflectionParameter { + +name: "arg1" + position: 0 + allowsNull: true + typeHint: "Symfony\Component\VarDumper\Tests\Fixtures\NotLoadableClass" +} +EOTXT + , $var + ); + } } public function testReflectionParameterScalar() @@ -422,7 +436,8 @@ public function testGenerator() $generator = new GeneratorDemo(); $generator = $generator->baz(); - $expectedDump = <<<'EODUMP' + if (\PHP_VERSION_ID < 80100) { + $expectedDump = <<<'EODUMP' Generator { this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} executing: { @@ -436,6 +451,23 @@ public function testGenerator() closed: false } EODUMP; + } else { + $expectedDump = <<<'EODUMP' +Generator { + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + trace: { + ./src/Symfony/Component/VarDumper/Tests/Fixtures/GeneratorDemo.php:13 { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() + › public function baz() + › { + › yield from bar(); + } + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} + } + closed: false +} +EODUMP; + } $this->assertDumpMatchesFormat($expectedDump, $generator); @@ -443,7 +475,8 @@ public function testGenerator() break; } - $expectedDump = <<<'EODUMP' + if (\PHP_VERSION_ID < 80100) { + $expectedDump = <<<'EODUMP' array:2 [ 0 => ReflectionGenerator { this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} @@ -472,6 +505,39 @@ public function testGenerator() } ] EODUMP; + } else { + $expectedDump = <<<'EODUMP' +array:2 [ + 0 => ReflectionGenerator { + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + trace: { + %s%eTests%eFixtures%eGeneratorDemo.php:9 { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() + › { + › yield 1; + › } + } + %s%eTests%eFixtures%eGeneratorDemo.php:20 { …} + %s%eTests%eFixtures%eGeneratorDemo.php:14 { …} + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} + } + closed: false + } + 1 => Generator { + trace: { + ./src/Symfony/Component/VarDumper/Tests/Fixtures/GeneratorDemo.php:9 { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() + › { + › yield 1; + › } + } + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() {} + } + closed: false + } +] +EODUMP; + } $r = new \ReflectionGenerator($generator); $this->assertDumpMatchesFormat($expectedDump, [$r, $r->getExecutingGenerator()]); diff --git a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php index 49b0cab3a2a0c..e5cc842817a14 100644 --- a/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php +++ b/src/Symfony/Component/VarExporter/Tests/VarExporterTest.php @@ -67,10 +67,13 @@ public function provideFailingSerialization() yield [$a]; - $a = [null, $h]; - $a[0] = &$a; + // This test segfaults on the final PHP 7.2 release + if (\PHP_VERSION_ID !== 70234) { + $a = [null, $h]; + $a[0] = &$a; - yield [$a]; + yield [$a]; + } } /** @@ -163,10 +166,13 @@ public function provideExport() yield ['hard-references', $value]; - $value = []; - $value[0] = &$value; + // This test segfaults on the final PHP 7.2 release + if (\PHP_VERSION_ID !== 70234) { + $value = []; + $value[0] = &$value; - yield ['hard-references-recursive', $value]; + yield ['hard-references-recursive', $value]; + } static $value = [123]; diff --git a/src/Symfony/Component/WebLink/Link.php b/src/Symfony/Component/WebLink/Link.php index c0402c6dc0952..ad4e7230741d4 100644 --- a/src/Symfony/Component/WebLink/Link.php +++ b/src/Symfony/Component/WebLink/Link.php @@ -46,7 +46,7 @@ class Link implements EvolvableLinkInterface private $rel = []; /** - * @var string[] + * @var array */ private $attributes = []; @@ -132,6 +132,8 @@ public function withoutRel($rel) /** * {@inheritdoc} * + * @param string|\Stringable|int|float|bool|string[] $value + * * @return static */ public function withAttribute($attribute, $value) diff --git a/src/Symfony/Component/Yaml/Inline.php b/src/Symfony/Component/Yaml/Inline.php index 3a6fe03e8dee6..e4f5e7008da89 100644 --- a/src/Symfony/Component/Yaml/Inline.php +++ b/src/Symfony/Component/Yaml/Inline.php @@ -127,6 +127,8 @@ public static function dump($value, int $flags = 0): string return self::dumpNull($flags); case $value instanceof \DateTimeInterface: return $value->format('c'); + case $value instanceof \UnitEnum: + return sprintf('!php/const %s::%s', \get_class($value), $value->name); case \is_object($value): if ($value instanceof TaggedValue) { return '!'.$value->getTag().' '.self::dump($value->getValue(), $flags); diff --git a/src/Symfony/Component/Yaml/Tests/Fixtures/FooUnitEnum.php b/src/Symfony/Component/Yaml/Tests/Fixtures/FooUnitEnum.php new file mode 100644 index 0000000000000..59092e27e8728 --- /dev/null +++ b/src/Symfony/Component/Yaml/Tests/Fixtures/FooUnitEnum.php @@ -0,0 +1,8 @@ +assertSame($expected, Inline::dump($dateTime)); } + /** + * @requires PHP 8.1 + */ + public function testDumpUnitEnum() + { + $this->assertSame("!php/const Symfony\Component\Yaml\Tests\Fixtures\FooUnitEnum::BAR", Inline::dump(FooUnitEnum::BAR)); + } + public function getDateTimeDumpTests() { $tests = []; diff --git a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php index 11ae4c8766a45..fdcc8ffe120a3 100644 --- a/src/Symfony/Contracts/Translation/Test/TranslatorTest.php +++ b/src/Symfony/Contracts/Translation/Test/TranslatorTest.php @@ -35,6 +35,7 @@ class TranslatorTest extends TestCase protected function setUp(): void { $this->defaultLocale = \Locale::getDefault(); + \Locale::setDefault('en'); } protected function tearDown(): void @@ -65,7 +66,6 @@ public function testTrans($expected, $id, $parameters) public function testTransChoiceWithExplicitLocale($expected, $id, $number) { $translator = $this->getTranslator(); - $translator->setLocale('en'); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); } @@ -77,8 +77,6 @@ public function testTransChoiceWithExplicitLocale($expected, $id, $number) */ public function testTransChoiceWithDefaultLocale($expected, $id, $number) { - \Locale::setDefault('en'); - $translator = $this->getTranslator(); $this->assertEquals($expected, $translator->trans($id, ['%count%' => $number])); @@ -87,7 +85,6 @@ public function testTransChoiceWithDefaultLocale($expected, $id, $number) public function testGetSetLocale() { $translator = $this->getTranslator(); - $translator->setLocale('en'); $this->assertEquals('en', $translator->getLocale()); }