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()); } pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy