diff --git a/.appveyor.yml b/.appveyor.yml index 67f2fcafc63f2..dbf83731eaffb 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -59,7 +59,10 @@ test_script: - SET SYMFONY_PHPUNIT_SKIPPED_TESTS=phpunit.skipped - copy /Y c:\php\php.ini-min c:\php\php.ini - IF %APPVEYOR_REPO_BRANCH% neq master (rm -Rf src\Symfony\Bridge\PhpUnit) + - mv src\Symfony\Component\HttpClient\phpunit.xml.dist src\Symfony\Component\HttpClient\phpunit.xml - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data || SET X=!errorlevel! + - php phpunit src\Symfony\Component\HttpClient || SET X=!errorlevel! - copy /Y c:\php\php.ini-max c:\php\php.ini - php phpunit src\Symfony --exclude-group tty,benchmark,intl-data || SET X=!errorlevel! + - php phpunit src\Symfony\Component\HttpClient || SET X=!errorlevel! - exit %X% diff --git a/.github/ISSUE_TEMPLATE/1_Bug_report.md b/.github/ISSUE_TEMPLATE/1_Bug_report.md index 4a64e16edf0a5..0e34075718894 100644 --- a/.github/ISSUE_TEMPLATE/1_Bug_report.md +++ b/.github/ISSUE_TEMPLATE/1_Bug_report.md @@ -1,6 +1,7 @@ --- name: 🐛 Bug Report about: Report errors and problems +labels: Bug --- diff --git a/.github/patch-types.php b/.github/patch-types.php index d9b1ed98f2bfe..f1ec07f725eb8 100644 --- a/.github/patch-types.php +++ b/.github/patch-types.php @@ -23,6 +23,7 @@ case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Compiler/OptionalServiceClass.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/ParentNotExists.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/Prototype/BadClasses/MissingParent.php'): + case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/WitherStaticReturnType.php'): case false !== strpos($file, '/src/Symfony/Component/DependencyInjection/Tests/Fixtures/php/'): case false !== strpos($file, '/src/Symfony/Component/ErrorHandler/Tests/Fixtures/'): case false !== strpos($file, '/src/Symfony/Component/PropertyInfo/Tests/Fixtures/Dummy.php'): diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000000000..7924d63c0578b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,101 @@ +name: Tests + +on: + push: + pull_request: + +jobs: + + integration: + name: Integration + runs-on: ubuntu-latest + + strategy: + matrix: + php: ['7.2', '7.4'] + + services: + redis: + image: redis:6.0.0 + ports: + - 6379:6379 + redis-cluster: + image: grokzen/redis-cluster:5.0.4 + ports: + - 7000:7000 + - 7001:7001 + - 7002:7002 + - 7003:7003 + - 7004:7004 + - 7005:7005 + - 7006:7006 + - 7007:7007 + env: + STANDALONE: true + memcached: + image: memcached:1.6.5 + ports: + - 11211:11211 + rabbitmq: + image: rabbitmq:3.8.3 + ports: + - 5672:5672 + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + coverage: "none" + extensions: "memcached,redis,xsl" + ini-values: "memory_limit=-1" + php-version: "${{ matrix.php }}" + tools: flex + + - name: Configure composer + run: | + ([ -d ~/.composer ] || mkdir ~/.composer) && cp .github/composer-config.json ~/.composer/config.json + SYMFONY_VERSION=$(cat composer.json | grep '^ *\"dev-master\". *\"[1-9]' | grep -o '[0-9.]*') + echo "::set-env name=SYMFONY_VERSION::$SYMFONY_VERSION" + echo "::set-env name=COMPOSER_ROOT_VERSION::$SYMFONY_VERSION.x-dev" + + - 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- + + - name: Install dependencies + run: | + echo "::group::composer update" + composer update --no-progress --no-suggest --ansi + echo "::endgroup::" + echo "::group::install phpunit" + ./phpunit install + echo "::endgroup::" + + - name: Run tests + run: ./phpunit --verbose --group integration + env: + SYMFONY_DEPRECATIONS_HELPER: 'max[indirect]=7' + REDIS_HOST: localhost + REDIS_CLUSTER_HOSTS: 'localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' + MESSENGER_REDIS_DSN: redis://127.0.0.1:7006/messages + MESSENGER_AMQP_DSN: amqp://localhost/%2f/messages + MEMCACHED_HOST: localhost + + - name: Run HTTP push tests + if: matrix.php == '7.4' + run: | + [ -d .phpunit ] && mv .phpunit .phpunit.bak + wget -q https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz && mv vulcain /usr/local/bin + docker run --rm -e COMPOSER_ROOT_VERSION -e SYMFONY_VERSION -v $(pwd):/app -v $(which composer):/usr/local/bin/composer -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.4-alpine ./phpunit --verbose src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push + sudo rm -rf .phpunit + [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit diff --git a/.travis.yml b/.travis.yml index 7e28a1fe8dc7f..c4ded6b5a91bb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,14 +13,11 @@ addons: - slapd - zookeeperd - libzookeeper-mt-dev - - rabbitmq-server env: global: - MIN_PHP=7.2.5 - SYMFONY_PROCESS_PHP_TEST_BINARY=~/.phpenv/shims/php - - MESSENGER_AMQP_DSN=amqp://localhost/%2f/messages - - MESSENGER_REDIS_DSN=redis://127.0.0.1:7006/messages - SYMFONY_PHPUNIT_DISABLE_RESULT_CACHE=1 matrix: @@ -39,28 +36,15 @@ cache: - php-$MIN_PHP - ~/php-ext -services: - - memcached - - mongodb - - redis-server - - rabbitmq - - docker - before_install: - | - # Enable Sury ppa + # Enable extra ppa sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys 6B05F25D762E3157 sudo add-apt-repository -y ppa:ondrej/php sudo rm /etc/apt/sources.list.d/google-chrome.list sudo rm /etc/apt/sources.list.d/mongodb-3.4.list sudo apt update - sudo apt install -y librabbitmq-dev libsodium-dev - - - | - # Start Redis cluster - docker pull grokzen/redis-cluster:5.0.4 - docker run -d -p 7000:7000 -p 7001:7001 -p 7002:7002 -p 7003:7003 -p 7004:7004 -p 7005:7005 -p 7006:7006 -p 7007:7007 -e "STANDALONE=true" --name redis-cluster grokzen/redis-cluster:5.0.4 - export REDIS_CLUSTER_HOSTS='localhost:7000 localhost:7001 localhost:7002 localhost:7003 localhost:7004 localhost:7005' + sudo apt install -y librabbitmq-dev libsodium-dev php-uuid zlib1g-dev - | # General configuration @@ -141,12 +125,6 @@ before_install: (cd php-$MIN_PHP && ./configure --enable-sigchild --enable-pcntl && make -j2) fi - - | - # Install vulcain - wget https://github.com/symfony/binary-utils/releases/download/v0.1/vulcain_0.1.3_Linux_x86_64.tar.gz -O - | tar xz - sudo mv vulcain /usr/local/bin - docker pull php:7.3-alpine - - | # php.ini configuration for PHP in $TRAVIS_PHP_VERSION $php_extra; do @@ -268,15 +246,6 @@ install: export PHP=$1 phpenv global $PHP - if [[ !$deps && $PHP = 7.2 ]]; then - phpenv global $PHP - tfold 'composer update' $COMPOSER_UP - [ -d .phpunit ] && mv .phpunit .phpunit.bak - tfold src/Symfony/Component/HttpClient.h2push "docker run -it --rm -v $(pwd):/app -v $(phpenv which composer):/usr/local/bin/composer -v /usr/local/bin/vulcain:/usr/local/bin/vulcain -w /app php:7.3-alpine ./phpunit src/Symfony/Component/HttpClient/Tests/CurlHttpClientTest.php --filter testHttp2Push" - sudo rm .phpunit -rf - [ -d .phpunit.bak ] && mv .phpunit.bak .phpunit - fi - if [[ $PHP != 7.4* && $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 diff --git a/CHANGELOG-4.4.md b/CHANGELOG-4.4.md index 68eceda5b9742..be58f04b95501 100644 --- a/CHANGELOG-4.4.md +++ b/CHANGELOG-4.4.md @@ -7,6 +7,56 @@ in 4.4 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.4.0...v4.4.1 +* 4.4.8 (2020-04-28) + + * bug #36536 [Cache] Allow invalidateTags calls to be traced by data collector (l-vo) + * bug #36566 [PhpUnitBridge] Use COMPOSER_BINARY env var if available (fancyweb) + * bug #36560 [YAML] escape DEL(\x7f) (sdkawata) + * bug #36539 [PhpUnitBridge] fix compatibility with phpunit 9 (garak) + * bug #36555 [Cache] skip APCu in chains when the backend is disabled (nicolas-grekas) + * bug #36523 [Form] apply automatically step=1 for datetime-local input (ottaviano) + * bug #36519 [FrameworkBundle] debug:autowiring: Fix wrong display when using class_alias (weaverryan) + * bug #36454 [DependencyInjection][ServiceSubscriber] Support late aliases (fancyweb) + * bug #36498 [Security/Core] fix escape for username in LdapBindAuthenticationProvider.php (stoccc) + * bug #36506 [FrameworkBundle] Fix session.attribute_bag service definition (fancyweb) + * bug #36500 [Routing][PrefixTrait] Add the _locale requirement (fancyweb) + * bug #36457 [Cache] CacheItem with tag is never a hit after expired (alexander-schranz, nicolas-grekas) + * bug #36490 [HttpFoundation] workaround PHP bug in the session module (nicolas-grekas) + * bug #36483 [SecurityBundle] fix accepting env vars in remember-me configurations (zek) + * bug #36343 [Form] Fixed handling groups sequence validation (HeahDude) + * bug #36460 [Cache] Avoid memory leak in TraceableAdapter::reset() (lyrixx) + * bug #36467 Mailer from sender fixes (fabpot) + * bug #36408 [PhpUnitBridge] add PolyfillTestCaseTrait::expectExceptionMessageMatches to provide FC with recent phpunit versions (soyuka) + * bug #36447 Remove return type for Twig function workflow_metadata() (gisostallenberg) + * bug #36449 [Messenger] Make sure redis transports are initialized correctly (Seldaek) + * bug #36411 [Form] RepeatedType should always have inner types mapped (biozshock) + * bug #36441 [DI] fix loading defaults when using the PHP-DSL (nicolas-grekas) + * bug #36434 [HttpKernel] silence E_NOTICE triggered since PHP 7.4 (xabbuh) + * bug #36365 [Validator] Fixed default group for nested composite constraints (HeahDude) + * bug #36422 [HttpClient] fix HTTP/2 support on non-SSL connections - CurlHttpClient only (nicolas-grekas) + * bug #36417 Force ping after transport exception (oesteve) + * bug #35591 [Validator] do not merge constraints within interfaces (greedyivan) + * bug #36377 [HttpClient] Fix scoped client without query option configuration (X-Coder264) + * bug #36387 [DI] fix detecting short service syntax in yaml (nicolas-grekas) + * bug #36392 [DI] add missing property declarations in InlineServiceConfigurator (nicolas-grekas) + * bug #36400 Allowing empty secrets to be set (weaverryan) + * bug #36380 [Process] Fixed input/output error on PHP 7.4 (mbardelmeijer) + * bug #36376 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) + * bug #36375 [Workflow] Use a strict comparison when retrieving raw marking in MarkingStore (lyrixx) + * bug #36305 [PropertyInfo][ReflectionExtractor] Check the array mutator prefixes last when the property is singular (fancyweb) + * bug #35656 [HttpFoundation] Fixed session migration with custom cookie lifetime (Guite) + * bug #36342 [HttpKernel][FrameworkBundle] fix compat with Debug component (nicolas-grekas) + * bug #36315 [WebProfilerBundle] Support for Content Security Policy style-src-elem and script-src-elem in WebProfiler (ampaze) + * bug #36286 [Validator] Allow URL-encoded special characters in basic auth part of URLs (cweiske) + * bug #36335 [Security] Track session usage whenever a new token is set (wouterj) + * bug #36332 [Serializer] Fix unitialized properties (from PHP 7.4.2) when serializing context for the cache key (alanpoulain) + * bug #36337 [MonologBridge] Fix $level type (fancyweb) + * bug #36223 [Security][Http][SwitchUserListener] Ignore all non existent username protection errors (fancyweb) + * bug #36239 [HttpKernel][LoggerDataCollector] Prevent keys collisions in the sanitized logs processing (fancyweb) + * bug #36245 [Validator] Fixed calling getters before resolving groups (HeahDude) + * bug #36265 Fix the reporting of deprecations in twig:lint (stof) + * bug #36283 [Security] forward multiple attributes voting flag (xabbuh) + * 4.4.7 (2020-03-30) * security #cve-2020-5255 [HttpFoundation] Do not set the default Content-Type based on the Accept header (yceruto) diff --git a/CHANGELOG-5.0.md b/CHANGELOG-5.0.md index 03f182478ebf3..1fb405767821a 100644 --- a/CHANGELOG-5.0.md +++ b/CHANGELOG-5.0.md @@ -630,7 +630,7 @@ To get the diff between two versions, go to https://github.com/symfony/symfony/c * feature #32446 [Lock] rename and deprecate Factory into LockFactory (Simperfit) * feature #31975 Dynamic bundle assets (garak) * feature #32429 [VarDumper] Let browsers trigger their own search on double CMD/CTRL + F (ogizanagi) - * feature #32198 [Lock] Split "StoreInterface" into multiple interfaces with less responsability (Simperfit) + * feature #32198 [Lock] Split "StoreInterface" into multiple interfaces with less responsibility (Simperfit) * feature #31511 [Validator] Allow to use property paths to get limits in range constraint (Lctrs) * feature #32424 [Console] don't redraw progress bar more than every 100ms by default (nicolas-grekas) * feature #27905 [MonologBridge] Monolog 2 compatibility (derrabus) diff --git a/CHANGELOG-5.1.md b/CHANGELOG-5.1.md new file mode 100644 index 0000000000000..cbe3b43b34030 --- /dev/null +++ b/CHANGELOG-5.1.md @@ -0,0 +1,236 @@ +CHANGELOG for 5.1.x +=================== + +This changelog references the relevant changes (bug and security fixes) done +in 5.1 minor versions. + +To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash +To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v5.1.0...v5.1.1 + +* 5.1.0-BETA1 (2020-05-05) + + * feature #36711 [Form] deprecate `NumberToLocalizedStringTransformer::ROUND_*` constants (nicolas-grekas) + * feature #36681 [FrameworkBundle] use the router context by default for assets (nicolas-grekas) + * feature #35545 [Serializer] Allow to include the severity in ConstraintViolationList (dunglas) + * feature #36471 [String] allow passing a string of custom characters to ByteString::fromRandom (azjezz) + * feature #35092 [Inflector][String] Move Inflector in String (fancyweb) + * feature #36302 [Form] Add the html5 option to ColorType to validate the input (fancyweb) + * feature #36184 [FrameworkBundle] Deprecate renderView() in favor of renderTemplate() (javiereguiluz) + * feature #36655 Automatically provide Messenger Doctrine schema to "diff" (weaverryan) + * feature #35849 [ExpressionLanguage] Added expression language syntax validator (Andrej-in-ua) + * feature #36656 [Security/Core] Add CustomUserMessageAccountStatusException (VincentLanglet) + * feature #36621 Log deprecations on a dedicated Monolog channel (l-vo) + * feature #34813 [Yaml] support YAML 1.2 octal notation, deprecate YAML 1.1 one (xabbuh) + * feature #36557 [Messenger] Add support for RecoverableException (jderusse) + * feature #36470 [DependencyInjection] Add a mechanism to deprecate public services to private (fancyweb) + * feature #36651 [FrameworkBundle] Allow configuring the default base URI with a DSN (nicolas-grekas) + * feature #36600 [Security] Added LDAP support to Authenticator system (wouterj) + * feature #35453 [Messenger] Add option to stop the worker after a message failed (micheh) + * feature #36094 [AmazonSqsMessenger] Use AsyncAws to handle SQS communication (jderusse) + * feature #36636 Add support of PHP8 static return type for withers (l-vo) + * feature #36586 [DI] allow loading and dumping tags with an attribute named "name" (nicolas-grekas) + * feature #36599 [HttpKernel] make kernels implementing `WarmableInterface` be part of the cache warmup stage (nicolas-grekas) + * feature #35992 [Mailer] Use AsyncAws to handle SES requests (jderusse) + * feature #36574 [Security] Removed anonymous in the new security system (wouterj) + * feature #36666 [Security] Renamed VerifyAuthenticatorCredentialsEvent to CheckPassportEvent (wouterj) + * feature #36575 [Security] Require entry_point to be configured with multiple authenticators (wouterj) + * feature #36570 [Security] Integrated Guards with the Authenticator system (wouterj) + * feature #36562 Revert "feature #30501 [FrameworkBundle][Routing] added Configurators to handle template and redirect controllers (HeahDude)" (nicolas-grekas) + * feature #36373 [DI] add syntax to stack decorators (nicolas-grekas) + * feature #36545 [DI] fix definition and usage of AbstractArgument (nicolas-grekas) + * feature #28744 [Serializer] Add an @Ignore annotation (dunglas) + * feature #36456 [String] Add locale-sensitive map for slugging symbols (lmasforne) + * feature #36535 [DI] skip preloading dependencies of non-preloaded services (nicolas-grekas) + * feature #36525 Improve SQS interoperability (jderusse) + * feature #36516 [Notifier] Throw an exception when the Slack DSN is not valid (fabpot) + * feature #35690 [Notifier] Add Free Mobile notifier (noniagriconomie) + * feature #33558 [Security] AuthenticatorManager to make "authenticators" first-class security (wouterj) + * feature #36187 [Routing] Deal with hosts per locale (odolbeau) + * feature #36464 [RedisMessengerBridge] Add a delete_after_ack option (Seldaek) + * feature #36431 [Messenger] Add FIFO support to the SQS transport (cv65kr) + * feature #36455 [Cache] Added context to log messages (Nyholm) + * feature #34363 [HttpFoundation] Add InputBag (azjezz) + * feature #36445 [WebProfilerBundle] Make a difference between queued and sent emails (fabpot) + * feature #36424 [Mailer][Messenger] add return statement for MessageHandler (ottaviano) + * feature #36426 [Form] Deprecated unused old `ServerParams` util (HeahDude) + * feature #36433 [Console] cursor tweaks (fabpot) + * feature #35828 [Notifier][Slack] Send messages using Incoming Webhooks (birkof, fabpot) + * feature #27444 [Console] Add Cursor class to control the cursor in the terminal (pierredup) + * feature #31390 [Serializer] UnwrappingDenormalizer (nonanerz) + * feature #36390 [DI] remove restriction and allow mixing "parent" and instanceof-conditionals/defaults/bindings (nicolas-grekas) + * feature #36388 [DI] deprecate the `inline()` function from the PHP-DSL in favor of `service()` (nicolas-grekas) + * feature #36389 [DI] allow decorators to reference their decorated service using the special `.inner` id (nicolas-grekas) + * feature #36345 [OptionsResolver] Improve the deprecation feature by handling package and version (atailouloute) + * feature #36372 [VarCloner] Cut Logger in dump (lyrixx) + * feature #35748 [HttpFoundation] Add support for all core response http control directives (azjezz) + * feature #36270 [FrameworkBundle] Add file links to named controllers in debug:router (chalasr) + * feature #35762 [Asset] Allows to download asset manifest over HTTP (GromNaN) + * feature #36195 [DI] add tags `container.preload`/`.no_preload` to declare extra classes to preload/services to not preload (nicolas-grekas) + * feature #36209 [HttpKernel] allow cache warmers to add to the list of preloaded classes and files (nicolas-grekas) + * feature #36243 [Security] Refactor logout listener to dispatch an event instead (wouterj) + * feature #36185 [Messenger] Add a \Throwable argument in RetryStrategyInterface methods (Benjamin Dos Santos) + * feature #35871 [Config] Improve the deprecation features by handling package and version (atailouloute) + * feature #35879 [DependencyInjection] Deprecate ContainerInterface aliases (fancyweb) + * feature #36273 [FrameworkBundle] Deprecate flashbag and attributebag services (William Arslett) + * feature #36257 [HttpKernel] Deprecate single-colon notation for controllers (chalasr) + * feature #35778 [DI] Improve the deprecation features by handling package and version (atailouloute) + * feature #36129 [HttpFoundation][HttpKernel][Security] Improve UnexpectedSessionUsageException backtrace (mtarld) + * feature #36186 [FrameworkBundle] Dump kernel extension configuration (guillbdx) + * feature #34984 [Form] Allowing plural message on extra data validation failure (popnikos) + * feature #36154 [Notifier][Slack] Add fields on Slack Section block (birkof) + * feature #36148 [Mailer][Mailgun] Support more headers (Nyholm) + * feature #36144 [FrameworkBundle][Routing] Add link to source to router:match (l-vo) + * feature #36117 [PropertyAccess][DX] Added an `UninitializedPropertyException` (HeahDude) + * feature #36088 [Form] Added "collection_entry" block prefix to CollectionType entries (HeahDude) + * feature #35936 [String] Add AbstractString::containsAny() (nicolas-grekas) + * feature #35744 [Validator] Add AtLeastOne constraint and validator (przemyslaw-bogusz) + * feature #35729 [Form] Correctly round model with PercentType and add a rounding_mode option (VincentLanglet) + * feature #35733 [Form] Added a "choice_filter" option to ChoiceType (HeahDude) + * feature #36003 [ErrorHandler][FrameworkBundle] better error messages in failing tests (guillbdx) + * feature #36034 [PhpUnitBridge] Deprecate @expectedDeprecation annotation (hkdobrev) + * feature #35924 [HttpClient] make `HttpClient::create()` return an `AmpHttpClient` when `amphp/http-client` is found but curl is not or too old (nicolas-grekas) + * feature #36072 [SecurityBundle] Added XSD for the extension configuration (HeahDude) + * feature #36074 [Uid] add AbstractUid and interop with base-58/32/RFC4122 encodings (nicolas-grekas) + * feature #36066 [Uid] use one class per type of UUID (nicolas-grekas) + * feature #36042 [Uid] add support for Ulid (nicolas-grekas) + * feature #35995 [FrameworkBundle] add --deprecations on debug:container command (Simperfit, noemi-salaun) + * feature #36059 [String] leverage Stringable from PHP 8 (nicolas-grekas) + * feature #35940 [UID] Added the component + Added support for UUID (lyrixx) + * feature #31375 [Form] Add label_html attribute (przemyslaw-bogusz) + * feature #35997 [DX][Testing] Added a loginUser() method to test protected resources (javiereguiluz, wouterj) + * feature #35978 [Messenger] Show message & handler(s) class description in debug:messenger (ogizanagi) + * feature #35960 [Security/Http] Hash Persistent RememberMe token (guillbdx) + * feature #35115 [HttpClient] Add portable HTTP/2 implementation based on Amp's HTTP client (nicolas-grekas) + * feature #35913 [LDAP] Add error code in exceptions generated by ldap (Victor Garcia) + * feature #35782 [Routing] Add stateless route attribute (mtarld) + * feature #35732 [FrameworkBundle][HttpKernel] Add session usage reporting in stateless mode (mtarld) + * feature #35815 [Validator] Allow Sequentially constraints on classes + target guards (ogizanagi) + * feature #35747 [Routing][FrameworkBundle] Allow using env() in route conditions (atailouloute) + * feature #35857 [Routing] deprecate RouteCompiler::REGEX_DELIMITER (nicolas-grekas) + * feature #35804 [HttpFoundation] Added MarshallingSessionHandler (atailouloute) + * feature #35858 [Security] Deprecated ROLE_PREVIOUS_ADMIN (wouterj) + * feature #35848 [Validator] add alpha3 option to Language constraint (xabbuh) + * feature #31189 [Security] Add IS_IMPERSONATOR, IS_ANONYMOUS and IS_REMEMBERED (HeahDude) + * feature #30994 [Form] Added support for caching choice lists based on options (HeahDude) + * feature #35783 [Validator] Add the divisibleBy option to the Count constraint (fancyweb) + * feature #35649 [String] Allow to keep the last word when truncating a text (franmomu) + * feature #34654 [Notifier] added Sinch texter transport (imiroslavov) + * feature #35673 [Process] Add getter for process starttime (dompie) + * feature #35689 [String] Transliterate & to and (Warxcell) + * feature #34550 [Form] Added an AbstractChoiceLoader to simplify implementations and handle global optimizations (HeahDude) + * feature #35688 [Notifier] Simplify OVH implementation (fabpot) + * feature #34540 [Notifier] add OvhCloud bridge (antiseptikk) + * feature #35192 [PhpUnitBridge] Add the ability to expect a deprecation inside a test (fancyweb) + * feature #35667 [DomCrawler] Rename UriExpander.php -> UriResolver (lyrixx) + * feature #35611 [Console] Moved estimated & remaining calculation logic to separate get method (peterjaap) + * feature #33968 [Notifier] Add Firebase bridge (Jeroeny) + * feature #34022 [Notifier] add RocketChat bridge (Jeroeny) + * feature #32454 [Messenger] Add SQS transport (jderusse) + * feature #33875 Add Mattermost notifier bridge (thePanz) + * feature #35400 [RFC][DX][OptionsResolver] Allow setting info message per option (yceruto) + * feature #30501 [FrameworkBundle][Routing] added Configurators to handle template and redirect controllers (HeahDude) + * feature #35373 [Translation] Support name attribute on the xliff2 translator loader (Taluu) + * feature #35550 Leverage trigger_deprecation() from symfony/deprecation-contracts (nicolas-grekas) + * feature #35648 [Contracts/Deprecation] don't use assert(), rename to trigger_deprecation() (nicolas-grekas) + * feature #33456 [MonologBridge] Add Mailer handler (BoShurik) + * feature #35384 [Messenger] Add receiving of old pending messages (redis) (toooni) + * feature #34456 [Validator] Add a constraint to sequentially validate a set of constraints (ogizanagi) + * feature #34334 [Validator] Allow to define a reusable set of constraints (ogizanagi) + * feature #35642 [HttpFoundation] Make dependency on Mime component optional (atailouloute) + * feature #35635 [HttpKernel] Make ErrorListener unaware of the event dispatcher (derrabus) + * feature #35019 [Cache] add SodiumMarshaller (atailouloute) + * feature #35625 [String] Add the s() helper method (fancyweb) + * feature #35624 [String] Remove the @experimental status (fancyweb) + * feature #33848 [OptionsResolver] Add new OptionConfigurator class to define options with fluent interface (lmillucci, yceruto) + * feature #35076 [DI] added possibility to define services with abstract arguments (Islam93) + * feature #35608 [Routing] add priority option to annotated routes (nicolas-grekas) + * feature #35526 [Contracts/Deprecation] Provide a generic function and convention to trigger deprecation notices (nicolas-grekas) + * feature #32747 [Form] Add "is empty callback" to form config (fancyweb) + * feature #34884 [DI] Enable auto alias compiler pass by default (X-Coder264) + * feature #35596 [Serializer] Add support for stdClass (dunglas) + * feature #34278 Update bootstrap_4_layout.html.twig (CoalaJoe) + * feature #31309 [SecurityBundle] add "service" option in remember_me firewall (Pchol) + * feature #31429 [Messenger] add support for abstract handlers (timiTao) + * feature #31466 [Validator] add Validation::createCallable() (janvernieuwe) + * feature #34747 [Notifier] Added possibility to extract path from provided DSN (espectrio) + * feature #35534 [FrameworkBundle] Use MailerAssertionsTrait in KernelTestCase (adrienfr) + * feature #35590 [FrameworkBundle] use framework.translator.enabled_locales to build routes' default "_locale" requirement (nicolas-grekas) + * feature #35167 [Notifier] Remove superfluous parameters in *Message::fromNotification() (fancyweb) + * feature #35415 Extracted code to expand an URI to `UriExpander` (lyrixx) + * feature #35485 [Messenger] Add support for PostgreSQL LISTEN/NOTIFY (dunglas) + * feature #32039 [Cache] Add couchbase cache adapter (ajcerezo) + * feature #32433 [Translation] Introduce a way to configure the enabled locales (javiereguiluz) + * feature #33003 [Filesystem] Add $suffix argument to tempnam() (jdufresne) + * feature #35347 [VarDumper] Add a RdKafka caster (romainneutron) + * feature #34925 Messenger: validate options for AMQP and Redis Connections (nikophil) + * feature #33315 [WebProfiler] Improve HttpClient Panel (ismail1432) + * feature #34298 [String] add LazyString to provide memoizing stringable objects (nicolas-grekas) + * feature #35368 [Yaml] Deprecate using the object and const tag without a value (fancyweb) + * feature #35566 [HttpClient] adding NoPrivateNetworkHttpClient decorator (hallboav) + * feature #35560 [HttpKernel] allow using public aliases to reference controllers (nicolas-grekas) + * feature #34871 [HttpClient] Allow pass array of callable to the mocking http client (Koc) + * feature #30704 [PropertyInfo] Add accessor and mutator extractor interface and implementation on reflection (joelwurtz, Korbeil) + * feature #35525 [Mailer] Randomize the first transport used by the RoundRobin transport (fabpot) + * feature #35116 [Validator] Add alpha3 option to country constraint (maxperrimond) + * feature #29139 [FrameworkBundle][TranslationDebug] Return non-zero exit code on failure (DAcodedBEAT) + * feature #35050 [Mailer] added tag/metadata support (kbond) + * feature #35215 [HttpFoundation] added withers to Cookie class (ns3777k) + * feature #35514 [DI][Routing] add wither to configure the path of PHP-DSL configurators (nicolas-grekas) + * feature #35519 [Mailer] Make default factories public (fabpot) + * feature #35156 [String] Made AbstractString::width() follow POSIX.1-2001 (fancyweb) + * feature #35308 [Dotenv] Add Dotenv::bootEnv() to check for .env.local.php before calling Dotenv::loadEnv() (nicolas-grekas) + * feature #35271 [PHPUnitBridge] Improved deprecations display (greg0ire) + * feature #35478 [Console] Add constants for main exit codes (Chi-teck) + * feature #35503 [Messenger] Add TLS option to Redis transport's DSN (Nyholm) + * feature #35262 [Mailer] add ability to disable the TLS peer verification via DSN (Aurélien Fontaine) + * feature #35194 [Mailer] read default timeout from ini configurations (azjezz) + * feature #35422 [Messenger] Move Transports to separate packages (Nyholm) + * feature #35425 [CssSelector] Added cache on top of CssSelectorConverter (lyrixx) + * feature #35362 [Cache] Add LRU + max-lifetime capabilities to ArrayCache (nicolas-grekas) + * feature #35402 [Console] Set Command::setHidden() final for adding default param in SF 6.0 (lyrixx) + * feature #35407 [HttpClient] collect the body of responses when possible (nicolas-grekas) + * feature #35391 [WebProfilerBundle][HttpClient] Added profiler links in the Web Profiler -> Http Client panel (cristagu) + * feature #35295 [Messenger] Messenger redis local sock dsn (JJarrie) + * feature #35322 [Workflow] Added a way to not fire the announce event (lyrixx) + * feature #35321 [Workflow] Make many internal services as hidden (lyrixx) + * feature #35235 [Serializer] Added scalar denormalization (a-menshchikov) + * feature #35310 [FrameworkBundle] Deprecate *not* setting the "framework.router.utf8" option (nicolas-grekas) + * feature #34387 [Yaml] Added yaml-lint binary (jschaedl) + * feature #35257 [FrameworkBundle] TemplateController should accept extra arguments to be sent to the template (Benjamin RICHARD) + * feature #34980 [Messenger] remove several messages with command messenger:failed:remove (nikophil) + * feature #35298 Make sure the UriSigner can be autowired (Toflar) + * feature #31518 [Validator] Added HostnameValidator (karser) + * feature #35284 Simplify UriSigner when working with HttpFoundation's Request (Toflar) + * feature #35285 [FrameworkBundle] Adding better output to secrets:decrypt-to-local command (weaverryan) + * feature #35273 [HttpClient] Add LoggerAwareInterface to ScopingHttpClient and TraceableHttpClient (pierredup) + * feature #34865 [FrameworkBundle][ContainerLintCommand] Style messages (fancyweb) + * feature #34847 Add support for safe HTTP preference - RFC 8674 (pyrech) + * feature #34880 [Twig][Form] Twig theme for Foundation 6 (Lyssal) + * feature #35281 [FrameworkBundle] Configure RequestContext through router config (benji07) + * feature #34819 [Console] Add SingleCommandApplication to ease creation of Single Command Application (lyrixx) + * feature #35104 [Messenger] Log sender alias in SendMessageMiddleware (ruudk) + * feature #35205 [Form] derive the view timezone from the model timezone (xabbuh) + * feature #34986 [Form] Added default `inputmode` attribute to Search, Email and Tel form types (fre5h) + * feature #35091 [String] Add the reverse() method (fancyweb) + * feature #35029 [DI] allow "." and "-" in env processor lines (nicolas-grekas) + * feature #34548 Added access decision strategy to respect voter priority (aschempp) + * feature #34881 [FrameworkBundle] Allow using the kernel as a registry of controllers and service factories (nicolas-grekas) + * feature #34977 [EventDispatcher] Deprecate LegacyEventDispatcherProxy (derrabus) + * feature #34873 [FrameworkBundle] Allow using a ContainerConfigurator in MicroKernelTrait::configureContainer() (nicolas-grekas) + * feature #34872 [FrameworkBundle] Added flex-compatible default implementations for `MicroKernelTrait::registerBundles()` and `getProjectDir()` (nicolas-grekas) + * feature #34916 [DI] Add support for defining method calls in InlineServiceConfigurator (Lctrs) + * feature #31889 [Lock] add mongodb store (kralos) + * feature #34924 [ErrorHandler] Enabled the dark theme for exception pages (javiereguiluz) + * feature #34769 [DependencyInjection] Autowire public typed properties (Plopix) + * feature #34856 [Validator] mark the Composite constraint as internal (xabbuh) + * feature #34771 Deprecate *Response::create() methods (fabpot) + * feature #32388 [Form] Allow to translate each language into its language in LanguageType (javiereguiluz) + * feature #34119 [Mime] Added MimeType for "msg" (LIBERT Jérémy) + * feature #34648 [Mailer] Allow to configure or disable the message bus to use (ogizanagi) + * feature #34705 [Validator] Label regex in date validator (kristofvc) + * feature #34591 [Workflow] Added `Registry::has()` to check if a workflow exists (lyrixx) + * feature #32937 [Routing] Deprecate RouteCollectionBuilder (vudaltsov) + * feature #34557 [PropertyInfo] Add support for typed properties (PHP 7.4) (dunglas) + * feature #34573 [DX] [Workflow] Added a way to specify a message when blocking a transition + better default message in case it is not set (lyrixx) + * feature #34457 Added context to exceptions thrown in apply method (koenreiniers) + diff --git a/UPGRADE-5.1.md b/UPGRADE-5.1.md new file mode 100644 index 0000000000000..faea3a8599f3c --- /dev/null +++ b/UPGRADE-5.1.md @@ -0,0 +1,189 @@ +UPGRADE FROM 5.0 to 5.1 +======================= + +Config +------ + + * The signature of method `NodeDefinition::setDeprecated()` has been updated to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `BaseNode::setDeprecated()` has been updated to `BaseNode::setDeprecation(string $package, string $version, string $message)`. + * Passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node is deprecated + * Deprecated `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead + +Console +------- + + * `Command::setHidden()` is final since Symfony 5.1 + +DependencyInjection +------------------- + + * The signature of method `Definition::setDeprecated()` has been updated to `Definition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `Alias::setDeprecated()` has been updated to `Alias::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `DeprecateTrait::deprecate()` has been updated to `DeprecateTrait::deprecation(string $package, string $version, string $message)`. + * Deprecated the `Psr\Container\ContainerInterface` and `Symfony\Component\DependencyInjection\ContainerInterface` aliases of the `service_container` service, + configure them explicitly instead. + * Deprecated `Definition::getDeprecationMessage()`, use `Definition::getDeprecation()` instead. + * Deprecated `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead. + * The `inline()` function from the PHP-DSL has been deprecated, use `service()` instead + +Dotenv +------ + + * Deprecated passing `$usePutenv` argument to Dotenv's constructor, use `Dotenv::usePutenv()` instead. + +EventDispatcher +--------------- + + * Deprecated `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. + +Form +---- + + * Not configuring the `rounding_mode` option of the `PercentType` is deprecated. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6. + * Not passing a rounding mode to the constructor of `PercentToLocalizedStringTransformer` is deprecated. It will default to `\NumberFormatter::ROUND_HALFUP` in Symfony 6. + * Implementing the `FormConfigInterface` without implementing the `getIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Implementing the `FormConfigBuilderInterface` without implementing the `setIsEmptyCallback()` method + is deprecated. The method will be added to the interface in 6.0. + * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()` - not defining them is deprecated. + * Using `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class is deprecated, use its parent `Symfony\Component\Form\Util\ServerParams` instead. + * The `NumberToLocalizedStringTransformer::ROUND_*` constants have been deprecated, use `\NumberFormatter::ROUND_*` instead. + +FrameworkBundle +--------------- + + * Deprecated passing a `RouteCollectionBuilder` to `MicroKernelTrait::configureRoutes()`, type-hint `RoutingConfigurator` instead + * Deprecated *not* setting the "framework.router.utf8" configuration option as it will default to `true` in Symfony 6.0 + * Deprecated `session.attribute_bag` service and `session.flash_bag` service. + +HttpFoundation +-------------- + + * Deprecate `Response::create()`, `JsonResponse::create()`, + `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use + `__construct()` instead) + * Made the Mime component an optional dependency + +HttpKernel +---------- + + * Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+ + not returning an array is deprecated + * Deprecated support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead. + +Inflector +--------- + + * The component has been deprecated, use `EnglishInflector` from the String component instead. + +Mailer +------ + + * Deprecated passing Mailgun headers without their "h:" prefix. + * Deprecated the `SesApiTransport` class. It has been replaced by SesApiAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes. + * Deprecated the `SesHttpTransport` class. It has been replaced by SesHttpAsyncAwsTransport Run `composer require async-aws/ses` to use the new classes. + +Messenger +--------- + + * Deprecated AmqpExt transport. It has moved to a separate package. Run `composer require symfony/amqp-messenger` to use the new classes. + * Deprecated Doctrine transport. It has moved to a separate package. Run `composer require symfony/doctrine-messenger` to use the new classes. + * Deprecated RedisExt transport. It has moved to a separate package. Run `composer require symfony/redis-messenger` to use the new classes. + * Deprecated use of invalid options in Redis and AMQP connections. + * Deprecated *not* declaring a `\Throwable` argument in `RetryStrategyInterface::isRetryable()` + * Deprecated *not* declaring a `\Throwable` argument in `RetryStrategyInterface::getWaitingTime()` + +Notifier +-------- + + * [BC BREAK] The `ChatMessage::fromNotification()` method's `$recipient` and `$transport` + arguments were removed. + * [BC BREAK] The `EmailMessage::fromNotification()` and `SmsMessage::fromNotification()` + methods' `$transport` argument was removed. + +OptionsResolver +--------------- + + * The signature of method `OptionsResolver::setDeprecated()` has been updated to `OptionsResolver::setDeprecated(string $option, string $package, string $version, $message)`. + * Deprecated `OptionsResolverIntrospector::getDeprecationMessage()`, use `OptionsResolverIntrospector::getDeprecation()` instead. + +PhpUnitBridge +------------- + + * Deprecated the `@expectedDeprecation` annotation, use the `ExpectDeprecationTrait::expectDeprecation()` method instead. + +Routing +------- + + * Deprecated `RouteCollectionBuilder` in favor of `RoutingConfigurator`. + * Added argument `$priority` to `RouteCollection::add()` + * Deprecated the `RouteCompiler::REGEX_DELIMITER` constant + +SecurityBundle +-------------- + + * Deprecated `anonymous: lazy` in favor of `lazy: true` + + *Before* + ```yaml + security: + firewalls: + main: + anonymous: lazy + ``` + + *After* + ```yaml + security: + firewalls: + main: + anonymous: true + lazy: true + ``` + * Marked the `AnonymousFactory`, `FormLoginFactory`, `FormLoginLdapFactory`, `GuardAuthenticationFactory`, + `HttpBasicFactory`, `HttpBasicLdapFactory`, `JsonLoginFactory`, `JsonLoginLdapFactory`, `RememberMeFactory`, `RemoteUserFactory` + and `X509Factory` as `@internal`. Instead of extending these classes, create your own implementation based on + `SecurityFactoryInterface`. + +Security +-------- + + * Deprecated `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute. + + *before* + ```twig + {% if is_granted('ROLE_PREVIOUS_ADMIN') %} + Exit impersonation + {% endif %} + ``` + + *after* + ```twig + {% if is_granted('IS_IMPERSONATOR') %} + Exit impersonation + {% endif %} + ``` + + * Deprecated `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead. + * Deprecated `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`. + +Yaml +---- + + * Added support for parsing numbers prefixed with `0o` as octal numbers. + * Deprecated support for parsing numbers starting with `0` as octal numbers. They will be parsed as strings as of Symfony 6.0. Prefix numbers with `0o` + so that they are parsed as octal numbers. + + Before: + + ```yaml + Yaml::parse('072'); + ``` + + After: + + ```yaml + Yaml::parse('0o72'); + ``` + + * Deprecated using the `!php/object` and `!php/const` tags without a value. diff --git a/UPGRADE-6.0.md b/UPGRADE-6.0.md new file mode 100644 index 0000000000000..1bece6f96a8b6 --- /dev/null +++ b/UPGRADE-6.0.md @@ -0,0 +1,135 @@ +UPGRADE FROM 5.x to 6.0 +======================= + +Config +------ + + * The signature of method `NodeDefinition::setDeprecated()` has been updated to `NodeDefinition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `BaseNode::setDeprecated()` has been updated to `BaseNode::setDeprecation(string $package, string $version, string $message)`. + * Passing a null message to `BaseNode::setDeprecated()` to un-deprecate a node is not supported anymore. + * Removed `BaseNode::getDeprecationMessage()`, use `BaseNode::getDeprecation()` instead. + +Console +------- + + * `Command::setHidden()` has a default value (`true`) for `$hidden` parameter + +DependencyInjection +------------------- + + * The signature of method `Definition::setDeprecated()` has been updated to `Definition::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `Alias::setDeprecated()` has been updated to `Alias::setDeprecation(string $package, string $version, string $message)`. + * The signature of method `DeprecateTrait::deprecate()` has been updated to `DeprecateTrait::deprecation(string $package, string $version, string $message)`. + * Removed the `Psr\Container\ContainerInterface` and `Symfony\Component\DependencyInjection\ContainerInterface` aliases of the `service_container` service, + configure them explicitly instead. + * Removed `Definition::getDeprecationMessage()`, use `Definition::getDeprecation()` instead. + * Removed `Alias::getDeprecationMessage()`, use `Alias::getDeprecation()` instead. + * The `inline()` function from the PHP-DSL has been removed, use `service()` instead + +Dotenv +------ + + * Removed argument `$usePutenv` from Dotenv's constructor, use `Dotenv::usePutenv()` instead. + +EventDispatcher +--------------- + + * Removed `LegacyEventDispatcherProxy`. Use the event dispatcher without the proxy. + +Form +---- + + * The default value of the `rounding_mode` option of the `PercentType` has been changed to `\NumberFormatter::ROUND_HALFUP`. + * The default rounding mode of the `PercentToLocalizedStringTransformer` has been changed to `\NumberFormatter::ROUND_HALFUP`. + * Added the `getIsEmptyCallback()` method to the `FormConfigInterface`. + * Added the `setIsEmptyCallback()` method to the `FormConfigBuilderInterface`. + * Added argument `callable|null $filter` to `ChoiceListFactoryInterface::createListFromChoices()` and `createListFromLoader()`. + * The `Symfony\Component\Form\Extension\Validator\Util\ServerParams` class has been removed, use its parent `Symfony\Component\Form\Util\ServerParams` instead. + * The `NumberToLocalizedStringTransformer::ROUND_*` constants have been removed, use `\NumberFormatter::ROUND_*` instead. + +FrameworkBundle +--------------- + + * `MicroKernelTrait::configureRoutes()` is now always called with a `RoutingConfigurator` + * The "framework.router.utf8" configuration option defaults to `true` + * Removed `session.attribute_bag` service and `session.flash_bag` service. + +HttpFoundation +-------------- + + * Removed `Response::create()`, `JsonResponse::create()`, + `RedirectResponse::create()`, and `StreamedResponse::create()` methods (use + `__construct()` instead) + +HttpKernel +---------- + + * Made `WarmableInterface::warmUp()` return a list of classes or files to preload on PHP 7.4+ + * Removed support for `service:action` syntax to reference controllers. Use `serviceOrFqcn::method` instead. + +Inflector +--------- + + * The component has been removed, use `EnglishInflector` from the String component instead. + +Mailer +------ + + * Removed the `SesApiTransport` class. Use `SesApiAsyncAwsTransport` instead. + * Removed the `SesHttpTransport` class. Use `SesHttpAsyncAwsTransport` instead. + +Messenger +--------- + + * Removed AmqpExt transport. Run `composer require symfony/amqp-messenger` to keep the transport in your application. + * Removed Doctrine transport. Run `composer require symfony/doctrine-messenger` to keep the transport in your application. + * Removed RedisExt transport. Run `composer require symfony/redis-messenger` to keep the transport in your application. + * Use of invalid options in Redis and AMQP connections now throws an error. + * The signature of method `RetryStrategyInterface::isRetryable()` has been updated to `RetryStrategyInterface::isRetryable(Envelope $message, \Throwable $throwable = null)`. + * The signature of method `RetryStrategyInterface::getWaitingTime()` has been updated to `RetryStrategyInterface::getWaitingTime(Envelope $message, \Throwable $throwable = null)`. + +OptionsResolver +--------------- + + * The signature of method `OptionsResolver::setDeprecated()` has been updated to `OptionsResolver::setDeprecated(string $option, string $package, string $version, $message)`. + * Removed `OptionsResolverIntrospector::getDeprecationMessage()`, use `OptionsResolverIntrospector::getDeprecation()` instead. + +PhpUnitBridge +------------- + + * Removed support for `@expectedDeprecation` annotations, use the `ExpectDeprecationTrait::expectDeprecation()` method instead. + +Routing +------- + + * Removed `RouteCollectionBuilder`. + * Added argument `$priority` to `RouteCollection::add()` + * Removed the `RouteCompiler::REGEX_DELIMITER` constant + +Security +-------- + + * Removed `ROLE_PREVIOUS_ADMIN` role in favor of `IS_IMPERSONATOR` attribute + * Removed `LogoutSuccessHandlerInterface` and `LogoutHandlerInterface`, register a listener on the `LogoutEvent` event instead. + * Removed `DefaultLogoutSuccessHandler` in favor of `DefaultLogoutListener`. + +Yaml +---- + + * Added support for parsing numbers prefixed with `0o` as octal numbers. + * Removed support for parsing numbers starting with `0` as octal numbers. They will be parsed as strings. Prefix numbers with `0o` + so that they are parsed as octal numbers. + + Before: + + ```yaml + Yaml::parse('072'); + ``` + + After: + + ```yaml + Yaml::parse('0o72'); + ``` + + * Removed support for using the `!php/object` and `!php/const` tags without a value. diff --git a/composer.json b/composer.json index 4e390fd55eedf..dab47277f9e8b 100644 --- a/composer.json +++ b/composer.json @@ -26,14 +26,16 @@ "psr/event-dispatcher": "^1.0", "psr/link": "^1.0", "psr/log": "~1.0", - "symfony/contracts": "^2", + "symfony/contracts": "^2.1", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-icu": "~1.0", "symfony/polyfill-intl-idn": "^1.10", "symfony/polyfill-intl-normalizer": "~1.0", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.11" + "symfony/polyfill-php73": "^1.11", + "symfony/polyfill-php80": "^1.15", + "symfony/polyfill-uuid": "^1.15" }, "replace": { "symfony/asset": "self.version", @@ -90,6 +92,7 @@ "symfony/translation": "self.version", "symfony/twig-bridge": "self.version", "symfony/twig-bundle": "self.version", + "symfony/uid": "self.version", "symfony/validator": "self.version", "symfony/var-dumper": "self.version", "symfony/var-exporter": "self.version", @@ -99,12 +102,16 @@ "symfony/yaml": "self.version" }, "require-dev": { + "amphp/http-client": "^4.2", + "amphp/http-tunnel": "^1.0", + "async-aws/ses": "^1.0", + "async-aws/sqs": "^1.0", "cache/integration-tests": "dev-master", "doctrine/annotations": "~1.0", "doctrine/cache": "~1.6", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "1.0.*", - "doctrine/dbal": "~2.4", + "doctrine/dbal": "~2.4,<=2.10.2", "doctrine/orm": "~2.4,>=2.4.5", "doctrine/reflection": "~1.0", "doctrine/doctrine-bundle": "^2.0", @@ -164,7 +171,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 7313d16d25c70..d2179994d502e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,9 @@ + + + @@ -75,6 +78,7 @@ Symfony\Component\Cache\Traits Symfony\Component\Console Symfony\Component\HttpFoundation + Symfony\Component\Uid diff --git a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php index 0a90bc1a3f86c..bca2ea2c170da 100644 --- a/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php +++ b/src/Symfony/Bridge/Doctrine/CacheWarmer/ProxyCacheWarmer.php @@ -43,9 +43,12 @@ public function isOptional() /** * {@inheritdoc} + * + * @return string[] A list of files to preload on PHP 7.4+ */ public function warmUp(string $cacheDir) { + $files = []; foreach ($this->registry->getManagers() as $em) { // we need the directory no matter the proxy cache generation strategy if (!is_dir($proxyCacheDir = $em->getConfiguration()->getProxyDir())) { @@ -64,6 +67,14 @@ public function warmUp(string $cacheDir) $classes = $em->getMetadataFactory()->getAllMetadata(); $em->getProxyFactory()->generateProxyClasses($classes); + + foreach (scandir($proxyCacheDir) as $file) { + if (!is_dir($file = $proxyCacheDir.'/'.$file)) { + $files[] = $file; + } + } } + + return $files; } } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php index cb09a3d2c1f77..99be884f34b04 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/DoctrineChoiceLoader.php @@ -12,27 +12,20 @@ namespace Symfony\Bridge\Doctrine\Form\ChoiceList; use Doctrine\Persistence\ObjectManager; -use Symfony\Component\Form\ChoiceList\ArrayChoiceList; -use Symfony\Component\Form\ChoiceList\ChoiceListInterface; -use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface; +use Symfony\Component\Form\ChoiceList\Loader\AbstractChoiceLoader; /** * Loads choices using a Doctrine object manager. * * @author Bernhard Schussek */ -class DoctrineChoiceLoader implements ChoiceLoaderInterface +class DoctrineChoiceLoader extends AbstractChoiceLoader { private $manager; private $class; private $idReader; private $objectLoader; - /** - * @var ChoiceListInterface - */ - private $choiceList; - /** * Creates a new choice loader. * @@ -59,81 +52,57 @@ public function __construct(ObjectManager $manager, string $class, IdReader $idR /** * {@inheritdoc} */ - public function loadChoiceList(callable $value = null) + protected function loadChoices(): iterable { - if ($this->choiceList) { - return $this->choiceList; - } - - $objects = $this->objectLoader + return $this->objectLoader ? $this->objectLoader->getEntities() : $this->manager->getRepository($this->class)->findAll(); - - return $this->choiceList = new ArrayChoiceList($objects, $value); } /** - * {@inheritdoc} + * @internal to be remove in Symfony 6 */ - public function loadValuesForChoices(array $choices, callable $value = null) + protected function doLoadValuesForChoices(array $choices): array { - // Performance optimization - if (empty($choices)) { - return []; - } - // Optimize performance for single-field identifiers. We already // know that the IDs are used as values - $optimize = $this->idReader && (null === $value || \is_array($value) && $value[0] === $this->idReader); - // Attention: This optimization does not check choices for existence - if ($optimize && !$this->choiceList) { - $values = []; - + if ($this->idReader) { + trigger_deprecation('symfony/doctrine-bridge', '5.1', 'Not defining explicitly the IdReader as value callback when query can be optimized is deprecated. Don\'t pass the IdReader to "%s" or define the "choice_value" option instead.', __CLASS__); // Maintain order and indices of the given objects + $values = []; foreach ($choices as $i => $object) { if ($object instanceof $this->class) { - // Make sure to convert to the right format - $values[$i] = (string) $this->idReader->getIdValue($object); + $values[$i] = $this->idReader->getIdValue($object); } } return $values; } - return $this->loadChoiceList($value)->getValuesForChoices($choices); + return parent::doLoadValuesForChoices($choices); } - /** - * {@inheritdoc} - */ - public function loadChoicesForValues(array $values, callable $value = null) + protected function doLoadChoicesForValues(array $values, ?callable $value): array { - // Performance optimization - // Also prevents the generation of "WHERE id IN ()" queries through the - // object loader. At least with MySQL and on the development machine - // this was tested on, no exception was thrown for such invalid - // statements, consequently no test fails when this code is removed. - // https://github.com/symfony/symfony/pull/8981#issuecomment-24230557 - if (empty($values)) { - return []; + $legacy = $this->idReader && null === $value; + + if ($legacy) { + trigger_deprecation('symfony/doctrine-bridge', '5.1', 'Not defining explicitly the IdReader as value callback when query can be optimized is deprecated. Don\'t pass the IdReader to "%s" or define the "choice_value" option instead.', __CLASS__); } // Optimize performance in case we have an object loader and // a single-field identifier - $optimize = $this->idReader && (null === $value || \is_array($value) && $this->idReader === $value[0]); - - if ($optimize && !$this->choiceList && $this->objectLoader) { - $unorderedObjects = $this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values); - $objectsById = []; + if (($legacy || \is_array($value) && $this->idReader === $value[0]) && $this->objectLoader) { $objects = []; + $objectsById = []; // Maintain order and indices from the given $values // An alternative approach to the following loop is to add the // "INDEX BY" clause to the Doctrine query in the loader, // but I'm not sure whether that's doable in a generic fashion. - foreach ($unorderedObjects as $object) { - $objectsById[(string) $this->idReader->getIdValue($object)] = $object; + foreach ($this->objectLoader->getEntitiesByIds($this->idReader->getIdField(), $values) as $object) { + $objectsById[$this->idReader->getIdValue($object)] = $object; } foreach ($values as $i => $id) { @@ -145,6 +114,6 @@ public function loadChoicesForValues(array $values, callable $value = null) return $objects; } - return $this->loadChoiceList($value)->getChoicesForValues($values); + return parent::doLoadChoicesForValues($values, $value); } } diff --git a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php index 195e7ce80f3f4..0625e5175ce08 100644 --- a/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php +++ b/src/Symfony/Bridge/Doctrine/Form/ChoiceList/IdReader.php @@ -84,16 +84,16 @@ public function isIntId(): bool * * This method assumes that the object has a single-column ID. * - * @return mixed The ID value + * @return string The ID value */ public function getIdValue(object $object = null) { if (!$object) { - return null; + return ''; } if (!$this->om->contains($object)) { - throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', \get_class($object))); + throw new RuntimeException(sprintf('Entity of type "%s" passed to the choice field must be managed. Maybe you forget to persist it in the entity manager?', get_debug_type($object))); } $this->om->initializeObject($object); @@ -104,7 +104,7 @@ public function getIdValue(object $object = null) $idValue = $this->associationIdReader->getIdValue($idValue); } - return $idValue; + return (string) $idValue; } /** diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php index 3348011e876ba..bccb1a9ebdc3e 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/DoctrineType.php @@ -20,6 +20,7 @@ use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer; use Symfony\Bridge\Doctrine\Form\EventListener\MergeDoctrineCollectionListener; use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\ChoiceList\ChoiceList; use Symfony\Component\Form\ChoiceList\Factory\CachingFactoryDecorator; use Symfony\Component\Form\Exception\RuntimeException; use Symfony\Component\Form\FormBuilderInterface; @@ -40,9 +41,9 @@ abstract class DoctrineType extends AbstractType implements ResetInterface private $idReaders = []; /** - * @var DoctrineChoiceLoader[] + * @var EntityLoaderInterface[] */ - private $choiceLoaders = []; + private $entityLoaders = []; /** * Creates the label for a choice. @@ -115,43 +116,26 @@ public function configureOptions(OptionsResolver $resolver) $choiceLoader = function (Options $options) { // Unless the choices are given explicitly, load them on demand if (null === $options['choices']) { - $hash = null; - $qbParts = null; + // If there is no QueryBuilder we can safely cache + $vary = [$options['em'], $options['class']]; - // If there is no QueryBuilder we can safely cache DoctrineChoiceLoader, // also if concrete Type can return important QueryBuilder parts to generate - // hash key we go for it as well - if (!$options['query_builder'] || null !== $qbParts = $this->getQueryBuilderPartsForCachingHash($options['query_builder'])) { - $hash = CachingFactoryDecorator::generateHash([ - $options['em'], - $options['class'], - $qbParts, - ]); - - if (isset($this->choiceLoaders[$hash])) { - return $this->choiceLoaders[$hash]; - } + // hash key we go for it as well, otherwise fallback on the instance + if ($options['query_builder']) { + $vary[] = $this->getQueryBuilderPartsForCachingHash($options['query_builder']) ?? $options['query_builder']; } - if (null !== $options['query_builder']) { - $entityLoader = $this->getLoader($options['em'], $options['query_builder'], $options['class']); - } else { - $queryBuilder = $options['em']->getRepository($options['class'])->createQueryBuilder('e'); - $entityLoader = $this->getLoader($options['em'], $queryBuilder, $options['class']); - } - - $doctrineChoiceLoader = new DoctrineChoiceLoader( + return ChoiceList::loader($this, new DoctrineChoiceLoader( $options['em'], $options['class'], $options['id_reader'], - $entityLoader - ); - - if (null !== $hash) { - $this->choiceLoaders[$hash] = $doctrineChoiceLoader; - } - - return $doctrineChoiceLoader; + $this->getCachedEntityLoader( + $options['em'], + $options['query_builder'] ?? $options['em']->getRepository($options['class'])->createQueryBuilder('e'), + $options['class'], + $vary + ) + ), $vary); } return null; @@ -162,7 +146,7 @@ public function configureOptions(OptionsResolver $resolver) // field name. We can only use numeric IDs as names, as we cannot // guarantee that a non-numeric ID contains a valid form name if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isIntId()) { - return [__CLASS__, 'createChoiceName']; + return ChoiceList::fieldName($this, [__CLASS__, 'createChoiceName']); } // Otherwise, an incrementing integer is used as name automatically @@ -176,7 +160,7 @@ public function configureOptions(OptionsResolver $resolver) $choiceValue = function (Options $options) { // If the entity has a single-column ID, use that ID as value if ($options['id_reader'] instanceof IdReader && $options['id_reader']->isSingleId()) { - return [$options['id_reader'], 'getIdValue']; + return ChoiceList::value($this, [$options['id_reader'], 'getIdValue'], $options['id_reader']); } // Otherwise, an incrementing integer is used as value automatically @@ -214,27 +198,13 @@ public function configureOptions(OptionsResolver $resolver) // Set the "id_reader" option via the normalizer. This option is not // supposed to be set by the user. $idReaderNormalizer = function (Options $options) { - $hash = CachingFactoryDecorator::generateHash([ - $options['em'], - $options['class'], - ]); - // The ID reader is a utility that is needed to read the object IDs // when generating the field values. The callback generating the // field values has no access to the object manager or the class // of the field, so we store that information in the reader. // The reader is cached so that two choice lists for the same class // (and hence with the same reader) can successfully be cached. - if (!isset($this->idReaders[$hash])) { - $classMetadata = $options['em']->getClassMetadata($options['class']); - $this->idReaders[$hash] = new IdReader($options['em'], $classMetadata); - } - - if ($this->idReaders[$hash]->isSingleId()) { - return $this->idReaders[$hash]; - } - - return null; + return $this->getCachedIdReader($options['em'], $options['class']); }; $resolver->setDefaults([ @@ -242,7 +212,7 @@ public function configureOptions(OptionsResolver $resolver) 'query_builder' => null, 'choices' => null, 'choice_loader' => $choiceLoader, - 'choice_label' => [__CLASS__, 'createChoiceLabel'], + 'choice_label' => ChoiceList::label($this, [__CLASS__, 'createChoiceLabel']), 'choice_name' => $choiceName, 'choice_value' => $choiceValue, 'id_reader' => null, // internal @@ -274,7 +244,28 @@ public function getParent() public function reset() { - $this->choiceLoaders = []; + $this->entityLoaders = []; + } + + private function getCachedIdReader(ObjectManager $manager, string $class): ?IdReader + { + $hash = CachingFactoryDecorator::generateHash([$manager, $class]); + + if (isset($this->idReaders[$hash])) { + return $this->idReaders[$hash]; + } + + $idReader = new IdReader($manager, $manager->getClassMetadata($class)); + + // don't cache the instance for composite ids that cannot be optimized + return $this->idReaders[$hash] = $idReader->isSingleId() ? $idReader : null; + } + + private function getCachedEntityLoader(ObjectManager $manager, $queryBuilder, string $class, array $vary): EntityLoaderInterface + { + $hash = CachingFactoryDecorator::generateHash($vary); + + return $this->entityLoaders[$hash] ?? ($this->entityLoaders[$hash] = $this->getLoader($manager, $queryBuilder, $class)); } } diff --git a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php index 8f04409ef5bf1..7cbe648f9b868 100644 --- a/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php +++ b/src/Symfony/Bridge/Doctrine/Form/Type/EntityType.php @@ -53,7 +53,7 @@ public function configureOptions(OptionsResolver $resolver) public function getLoader(ObjectManager $manager, $queryBuilder, string $class) { if (!$queryBuilder instanceof QueryBuilder) { - throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder))); } return new ORMQueryBuilderLoader($queryBuilder); @@ -79,7 +79,7 @@ public function getBlockPrefix() public function getQueryBuilderPartsForCachingHash($queryBuilder): ?array { if (!$queryBuilder instanceof QueryBuilder) { - throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, \is_object($queryBuilder) ? \get_class($queryBuilder) : \gettype($queryBuilder))); + throw new \TypeError(sprintf('Expected an instance of "%s", but got "%s".', QueryBuilder::class, get_debug_type($queryBuilder))); } return [ diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php new file mode 100644 index 0000000000000..96699cb130980 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/MessengerTransportDoctrineSchemaSubscriber.php @@ -0,0 +1,96 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\DBAL\Event\SchemaCreateTableEventArgs; +use Doctrine\DBAL\Events; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; +use Symfony\Component\Messenger\Transport\TransportInterface; + +/** + * Automatically adds any required database tables to the Doctrine Schema. + * + * @author Ryan Weaver + */ +final class MessengerTransportDoctrineSchemaSubscriber implements EventSubscriber +{ + private const PROCESSING_TABLE_FLAG = self::class.':processing'; + + private $transports; + + /** + * @param iterable|TransportInterface[] $transports + */ + public function __construct(iterable $transports) + { + $this->transports = $transports; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + foreach ($this->transports as $transport) { + if (!$transport instanceof DoctrineTransport) { + continue; + } + + $transport->configureSchema($event->getSchema(), $dbalConnection); + } + } + + public function onSchemaCreateTable(SchemaCreateTableEventArgs $event): void + { + $table = $event->getTable(); + + // if this method triggers a nested create table below, allow Doctrine to work like normal + if ($table->hasOption(self::PROCESSING_TABLE_FLAG)) { + return; + } + + foreach ($this->transports as $transport) { + if (!$transport instanceof DoctrineTransport) { + continue; + } + + $extraSql = $transport->getExtraSetupSqlForTable($table); + if (null === $extraSql) { + continue; + } + + // avoid this same listener from creating a loop on this table + $table->addOption(self::PROCESSING_TABLE_FLAG, true); + $createTableSql = $event->getPlatform()->getCreateTableSQL($table); + + /* + * Add all the SQL needed to create the table and tell Doctrine + * to "preventDefault" so that only our SQL is used. This is + * the only way to inject some extra SQL. + */ + $event->addSql($createTableSql); + $event->addSql($extraSql); + $event->preventDefault(); + + return; + } + } + + public function getSubscribedEvents(): array + { + return [ + ToolEvents::postGenerateSchema, + Events::onSchemaCreateTable, + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php b/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php new file mode 100644 index 0000000000000..41330e7971b5a --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriber.php @@ -0,0 +1,50 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\SchemaListener; + +use Doctrine\Common\EventSubscriber; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use Doctrine\ORM\Tools\ToolEvents; +use Symfony\Component\Cache\Adapter\PdoAdapter; + +/** + * Automatically adds the cache table needed for the PdoAdapter. + * + * @author Ryan Weaver + */ +final class PdoCacheAdapterDoctrineSchemaSubscriber implements EventSubscriber +{ + private $pdoAdapters; + + /** + * @param iterable|PdoAdapter[] $pdoAdapters + */ + public function __construct(iterable $pdoAdapters) + { + $this->pdoAdapters = $pdoAdapters; + } + + public function postGenerateSchema(GenerateSchemaEventArgs $event): void + { + $dbalConnection = $event->getEntityManager()->getConnection(); + foreach ($this->pdoAdapters as $pdoAdapter) { + $pdoAdapter->configureSchema($event->getSchema(), $dbalConnection); + } + } + + public function getSubscribedEvents(): array + { + return [ + ToolEvents::postGenerateSchema, + ]; + } +} diff --git a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php index b1fec89674f1a..defc2cb2af438 100644 --- a/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php +++ b/src/Symfony/Bridge/Doctrine/Security/User/EntityUserProvider.php @@ -55,7 +55,7 @@ public function loadUserByUsername(string $username) $user = $repository->findOneBy([$this->property => $username]); } else { if (!$repository instanceof UserLoaderInterface) { - throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, \get_class($repository))); + throw new \InvalidArgumentException(sprintf('You must either make the "%s" entity Doctrine Repository ("%s") implement "Symfony\Bridge\Doctrine\Security\User\UserLoaderInterface" or set the "property" option in the corresponding entity provider configuration.', $this->classOrAlias, get_debug_type($repository))); } $user = $repository->loadUserByUsername($username); @@ -75,7 +75,7 @@ public function refreshUser(UserInterface $user) { $class = $this->getClass(); if (!$user instanceof $class) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } $repository = $this->getRepository(); @@ -114,7 +114,7 @@ public function upgradePassword(UserInterface $user, string $newEncodedPassword) { $class = $this->getClass(); if (!$user instanceof $class) { - throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user))); + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_debug_type($user))); } $repository = $this->getRepository(); diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php index 182f2703e0ac3..3966be4965f37 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/ChoiceList/DoctrineChoiceLoaderTest.php @@ -19,6 +19,7 @@ use Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader; use Symfony\Bridge\Doctrine\Form\ChoiceList\EntityLoaderInterface; use Symfony\Bridge\Doctrine\Form\ChoiceList\IdReader; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Form\ChoiceList\ArrayChoiceList; use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface; @@ -27,6 +28,8 @@ */ class DoctrineChoiceLoaderTest extends TestCase { + use ExpectDeprecationTrait; + /** * @var ChoiceListFactoryInterface|MockObject */ @@ -100,6 +103,10 @@ protected function setUp(): void ->method('getClassMetadata') ->with($this->class) ->willReturn(new ClassMetadata($this->class)); + $this->repository->expects($this->any()) + ->method('findAll') + ->willReturn([$this->obj1, $this->obj2, $this->obj3]) + ; } public function testLoadChoiceList() @@ -186,8 +193,12 @@ public function testLoadValuesForChoicesDoesNotLoadIfEmptyChoices() $this->assertSame([], $loader->loadValuesForChoices([])); } + /** + * @group legacy + */ public function testLoadValuesForChoicesDoesNotLoadIfSingleIntId() { + $this->expectDeprecation('Since symfony/doctrine-bridge 5.1: Not defining explicitly the IdReader as value callback when query can be optimized is deprecated. Don\'t pass the IdReader to "Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader" or define the "choice_value" option instead.'); $loader = new DoctrineChoiceLoader( $this->om, $this->class, @@ -205,7 +216,7 @@ public function testLoadValuesForChoicesDoesNotLoadIfSingleIntId() $this->assertSame(['2'], $loader->loadValuesForChoices([$this->obj2])); } - public function testLoadValuesForChoicesLoadsIfSingleIntIdAndValueGiven() + public function testLoadValuesForChoicesDoesNotLoadIfSingleIntIdAndValueGiven() { $loader = new DoctrineChoiceLoader( $this->om, @@ -216,7 +227,7 @@ public function testLoadValuesForChoicesLoadsIfSingleIntIdAndValueGiven() $choices = [$this->obj1, $this->obj2, $this->obj3]; $value = function (\stdClass $object) { return $object->name; }; - $this->repository->expects($this->once()) + $this->repository->expects($this->never()) ->method('findAll') ->willReturn($choices); @@ -254,8 +265,7 @@ public function testLoadChoicesForValues() { $loader = new DoctrineChoiceLoader( $this->om, - $this->class, - $this->idReader + $this->class ); $choices = [$this->obj1, $this->obj2, $this->obj3]; @@ -285,8 +295,12 @@ public function testLoadChoicesForValuesDoesNotLoadIfEmptyValues() $this->assertSame([], $loader->loadChoicesForValues([])); } - public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() + /** + * @group legacy + */ + public function legacyTestLoadChoicesForValuesLoadsOnlyChoicesIfValueUseIdReader() { + $this->expectDeprecation('Not defining explicitly the IdReader as value callback when query can be optimized has been deprecated in 5.1. Don\'t pass the IdReader to "Symfony\Bridge\Doctrine\Form\ChoiceList\DoctrineChoiceLoader" or define the choice_value instead.'); $loader = new DoctrineChoiceLoader( $this->om, $this->class, @@ -321,6 +335,42 @@ public function testLoadChoicesForValuesLoadsOnlyChoicesIfSingleIntId() )); } + public function testLoadChoicesForValuesLoadsOnlyChoicesIfValueUseIdReader() + { + $loader = new DoctrineChoiceLoader( + $this->om, + $this->class, + $this->idReader, + $this->objectLoader + ); + + $choices = [$this->obj2, $this->obj3]; + + $this->idReader->expects($this->any()) + ->method('getIdField') + ->willReturn('idField'); + + $this->repository->expects($this->never()) + ->method('findAll'); + + $this->objectLoader->expects($this->once()) + ->method('getEntitiesByIds') + ->with('idField', [4 => '3', 7 => '2']) + ->willReturn($choices); + + $this->idReader->expects($this->any()) + ->method('getIdValue') + ->willReturnMap([ + [$this->obj2, '2'], + [$this->obj3, '3'], + ]); + + $this->assertSame( + [4 => $this->obj3, 7 => $this->obj2], + $loader->loadChoicesForValues([4 => '3', 7 => '2'], [$this->idReader, 'getIdValue'] + )); + } + public function testLoadChoicesForValuesLoadsAllIfSingleIntIdAndValueGiven() { $loader = new DoctrineChoiceLoader( diff --git a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php index ec8f7933f9a9b..ec51c708aec03 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php +++ b/src/Symfony/Bridge/Doctrine/Tests/Form/Type/EntityTypeTest.php @@ -1205,13 +1205,13 @@ public function testLoaderCaching() 'property3' => 2, ]); - $choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader'); - $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); - $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); + $choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list'); - $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1); - $this->assertSame($choiceLoader1, $choiceLoader2); - $this->assertSame($choiceLoader1, $choiceLoader3); + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } public function testLoaderCachingWithParameters() @@ -1265,13 +1265,13 @@ public function testLoaderCachingWithParameters() 'property3' => 2, ]); - $choiceLoader1 = $form->get('property1')->getConfig()->getOption('choice_loader'); - $choiceLoader2 = $form->get('property2')->getConfig()->getOption('choice_loader'); - $choiceLoader3 = $form->get('property3')->getConfig()->getOption('choice_loader'); + $choiceList1 = $form->get('property1')->getConfig()->getAttribute('choice_list'); + $choiceList2 = $form->get('property2')->getConfig()->getAttribute('choice_list'); + $choiceList3 = $form->get('property3')->getConfig()->getAttribute('choice_list'); - $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface', $choiceLoader1); - $this->assertSame($choiceLoader1, $choiceLoader2); - $this->assertSame($choiceLoader1, $choiceLoader3); + $this->assertInstanceOf('Symfony\Component\Form\ChoiceList\LazyChoiceList', $choiceList1); + $this->assertSame($choiceList1, $choiceList2); + $this->assertSame($choiceList1, $choiceList3); } protected function createRegistryMock($name, $em) diff --git a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php index 6e4807c58fb7a..e8f2b454cfa95 100644 --- a/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php +++ b/src/Symfony/Bridge/Doctrine/Tests/PropertyInfo/Fixtures/DoctrineFooType.php @@ -50,7 +50,7 @@ public function convertToDatabaseValue($value, AbstractPlatform $platform) return null; } if (!$value instanceof Foo) { - throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', \gettype($value))); + throw new ConversionException(sprintf('Expected "%s", got "%s"', 'Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\Foo', get_debug_type($value))); } return $foo->bar; diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php new file mode 100644 index 0000000000000..6bff7c0d395d3 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/MessengerTransportDoctrineSchemaSubscriberTest.php @@ -0,0 +1,99 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\SchemaListener; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Event\SchemaCreateTableEventArgs; +use Doctrine\DBAL\Platforms\AbstractPlatform; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\DBAL\Schema\Table; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\SchemaListener\MessengerTransportDoctrineSchemaSubscriber; +use Symfony\Component\Messenger\Bridge\Doctrine\Transport\DoctrineTransport; +use Symfony\Component\Messenger\Transport\TransportInterface; + +class MessengerTransportDoctrineSchemaSubscriberTest extends TestCase +{ + public function testPostGenerateSchema() + { + $schema = new Schema(); + $dbalConnection = $this->createMock(Connection::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getConnection') + ->willReturn($dbalConnection); + $event = new GenerateSchemaEventArgs($entityManager, $schema); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('configureSchema') + ->with($schema, $dbalConnection); + $otherTransport = $this->createMock(TransportInterface::class); + $otherTransport->expects($this->never()) + ->method($this->anything()); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport, $otherTransport]); + $subscriber->postGenerateSchema($event); + } + + public function testOnSchemaCreateTable() + { + $platform = $this->createMock(AbstractPlatform::class); + $table = new Table('queue_table'); + $event = new SchemaCreateTableEventArgs($table, [], [], $platform); + + $otherTransport = $this->createMock(TransportInterface::class); + $otherTransport->expects($this->never()) + ->method($this->anything()); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('getExtraSetupSqlForTable') + ->with($table) + ->willReturn('ALTER TABLE pizza ADD COLUMN extra_cheese boolean'); + + // we use the platform to generate the full create table sql + $platform->expects($this->once()) + ->method('getCreateTableSQL') + ->with($table) + ->willReturn('CREATE TABLE pizza (id integer NOT NULL)'); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$otherTransport, $doctrineTransport]); + $subscriber->onSchemaCreateTable($event); + $this->assertTrue($event->isDefaultPrevented()); + $this->assertSame([ + 'CREATE TABLE pizza (id integer NOT NULL)', + 'ALTER TABLE pizza ADD COLUMN extra_cheese boolean', + ], $event->getSql()); + } + + public function testOnSchemaCreateTableNoExtraSql() + { + $platform = $this->createMock(AbstractPlatform::class); + $table = new Table('queue_table'); + $event = new SchemaCreateTableEventArgs($table, [], [], $platform); + + $doctrineTransport = $this->createMock(DoctrineTransport::class); + $doctrineTransport->expects($this->once()) + ->method('getExtraSetupSqlForTable') + ->willReturn(null); + + $platform->expects($this->never()) + ->method('getCreateTableSQL'); + + $subscriber = new MessengerTransportDoctrineSchemaSubscriber([$doctrineTransport]); + $subscriber->onSchemaCreateTable($event); + $this->assertFalse($event->isDefaultPrevented()); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php new file mode 100644 index 0000000000000..9cf70e943ed25 --- /dev/null +++ b/src/Symfony/Bridge/Doctrine/Tests/SchemaListener/PdoCacheAdapterDoctrineSchemaSubscriberTest.php @@ -0,0 +1,42 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Doctrine\Tests\SchemaListener; + +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Schema\Schema; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Doctrine\SchemaListener\PdoCacheAdapterDoctrineSchemaSubscriber; +use Symfony\Component\Cache\Adapter\PdoAdapter; + +class PdoCacheAdapterDoctrineSchemaSubscriberTest extends TestCase +{ + public function testPostGenerateSchema() + { + $schema = new Schema(); + $dbalConnection = $this->createMock(Connection::class); + $entityManager = $this->createMock(EntityManagerInterface::class); + $entityManager->expects($this->once()) + ->method('getConnection') + ->willReturn($dbalConnection); + $event = new GenerateSchemaEventArgs($entityManager, $schema); + + $pdoAdapter = $this->createMock(PdoAdapter::class); + $pdoAdapter->expects($this->once()) + ->method('configureSchema') + ->with($schema, $dbalConnection); + + $subscriber = new PdoCacheAdapterDoctrineSchemaSubscriber([$pdoAdapter]); + $subscriber->postGenerateSchema($event); + } +} diff --git a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php index 35f723aa88d9f..cbf40af2b5750 100644 --- a/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php +++ b/src/Symfony/Bridge/Doctrine/Validator/Constraints/UniqueEntityValidator.php @@ -71,7 +71,7 @@ public function validate($entity, Constraint $constraint) $em = $this->registry->getManagerForClass(\get_class($entity)); if (!$em) { - throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', \get_class($entity))); + throw new ConstraintDefinitionException(sprintf('Unable to find the object manager associated with an entity of class "%s".', get_debug_type($entity))); } } diff --git a/src/Symfony/Bridge/Doctrine/composer.json b/src/Symfony/Bridge/Doctrine/composer.json index bf8b981f583c3..57ef8de15d3e1 100644 --- a/src/Symfony/Bridge/Doctrine/composer.json +++ b/src/Symfony/Bridge/Doctrine/composer.json @@ -21,15 +21,18 @@ "doctrine/persistence": "^1.3", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-mbstring": "~1.0", + "symfony/polyfill-php80": "^1.15", "symfony/service-contracts": "^1.1|^2" }, "require-dev": { "symfony/stopwatch": "^4.4|^5.0", + "symfony/cache": "^5.1", "symfony/config": "^4.4|^5.0", "symfony/dependency-injection": "^4.4|^5.0", - "symfony/form": "^5.0", + "symfony/form": "^5.1", "symfony/http-kernel": "^5.0", "symfony/messenger": "^4.4|^5.0", + "symfony/doctrine-messenger": "^5.1", "symfony/property-access": "^4.4|^5.0", "symfony/property-info": "^5.0", "symfony/proxy-manager-bridge": "^4.4|^5.0", @@ -42,14 +45,14 @@ "doctrine/cache": "~1.6", "doctrine/collections": "~1.0", "doctrine/data-fixtures": "1.0.*", - "doctrine/dbal": "~2.4", + "doctrine/dbal": "~2.4,<=2.10.2", "doctrine/orm": "^2.6.3", "doctrine/reflection": "~1.0" }, "conflict": { "phpunit/phpunit": "<5.4.3", "symfony/dependency-injection": "<4.4", - "symfony/form": "<5", + "symfony/form": "<5.1", "symfony/http-kernel": "<5", "symfony/messenger": "<4.4", "symfony/property-info": "<5", @@ -74,7 +77,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } } } diff --git a/src/Symfony/Bridge/Monolog/CHANGELOG.md b/src/Symfony/Bridge/Monolog/CHANGELOG.md index de5980cddaf0b..2a4d31a2ab340 100644 --- a/src/Symfony/Bridge/Monolog/CHANGELOG.md +++ b/src/Symfony/Bridge/Monolog/CHANGELOG.md @@ -1,6 +1,10 @@ CHANGELOG ========= +5.1.0 +----- + * Added `MailerHandler` + 5.0.0 ----- diff --git a/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php new file mode 100644 index 0000000000000..cf59f45ef388f --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Handler/MailerHandler.php @@ -0,0 +1,144 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use Monolog\Handler\AbstractProcessingHandler; +use Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +/** + * @author Alexander Borisov + */ +class MailerHandler extends AbstractProcessingHandler +{ + private $mailer; + + private $messageTemplate; + + /** + * @param callable|Email $messageTemplate + * @param string|int $level The minimum logging level at which this handler will be triggered + */ + public function __construct(MailerInterface $mailer, $messageTemplate, $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + + $this->mailer = $mailer; + $this->messageTemplate = $messageTemplate; + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $messages = []; + + foreach ($records as $record) { + if ($record['level'] < $this->level) { + continue; + } + $messages[] = $this->processRecord($record); + } + + if (!empty($messages)) { + $this->send((string) $this->getFormatter()->formatBatch($messages), $messages); + } + } + + /** + * {@inheritdoc} + */ + protected function write(array $record): void + { + $this->send((string) $record['formatted'], [$record]); + } + + /** + * Send a mail with the given content. + * + * @param string $content formatted email body to be sent + * @param array $records the array of log records that formed this content + */ + protected function send(string $content, array $records) + { + $this->mailer->send($this->buildMessage($content, $records)); + } + + /** + * Gets the formatter for the Message subject. + * + * @param string $format The format of the subject + */ + protected function getSubjectFormatter(string $format): FormatterInterface + { + return new LineFormatter($format); + } + + /** + * Creates instance of Message to be sent. + * + * @param string $content formatted email body to be sent + * @param array $records Log records that formed the content + */ + protected function buildMessage(string $content, array $records): Email + { + $message = null; + if ($this->messageTemplate instanceof Email) { + $message = clone $this->messageTemplate; + } elseif (\is_callable($this->messageTemplate)) { + $message = \call_user_func($this->messageTemplate, $content, $records); + if (!$message instanceof Email) { + throw new \InvalidArgumentException(sprintf('Could not resolve message from a callable. Instance of "%s" is expected.', Email::class)); + } + } else { + throw new \InvalidArgumentException('Could not resolve message as instance of Email or a callable returning it.'); + } + + if ($records) { + $subjectFormatter = $this->getSubjectFormatter($message->getSubject()); + $message->subject($subjectFormatter->format($this->getHighestRecord($records))); + } + + if ($this->getFormatter() instanceof HtmlFormatter) { + if ($message->getHtmlCharset()) { + $message->html($content, $message->getHtmlCharset()); + } else { + $message->html($content); + } + } else { + if ($message->getTextCharset()) { + $message->text($content, $message->getTextCharset()); + } else { + $message->text($content); + } + } + + return $message; + } + + protected function getHighestRecord(array $records): array + { + $highestRecord = null; + foreach ($records as $record) { + if (null === $highestRecord || $highestRecord['level'] < $record['level']) { + $highestRecord = $record; + } + } + + return $highestRecord; + } +} diff --git a/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php new file mode 100644 index 0000000000000..24aaa6b95cdd9 --- /dev/null +++ b/src/Symfony/Bridge/Monolog/Tests/Handler/MailerHandlerTest.php @@ -0,0 +1,123 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\Monolog\Tests\Handler; + +use Monolog\Formatter\HtmlFormatter; +use Monolog\Formatter\LineFormatter; +use PHPUnit\Framework\MockObject\MockObject; +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\Monolog\Handler\MailerHandler; +use Symfony\Bridge\Monolog\Logger; +use Symfony\Component\Mailer\MailerInterface; +use Symfony\Component\Mime\Email; + +class MailerHandlerTest extends TestCase +{ + /** @var MockObject|MailerInterface */ + private $mailer = null; + + protected function setUp(): void + { + $this->mailer = $this->createMock(MailerInterface::class); + } + + public function testHandle() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + public function testHandleBatch() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new LineFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: ERROR error' === $email->getSubject() && null === $email->getHtmlBody(); + })) + ; + $handler->handleBatch($this->getMultipleRecords()); + } + + public function testMessageCreationIsLazyWhenUsingCallback() + { + $this->mailer + ->expects($this->never()) + ->method('send') + ; + + $callback = function () { + throw new \RuntimeException('Email creation callback should not have been called in this test'); + }; + $handler = new MailerHandler($this->mailer, $callback, Logger::ALERT); + + $records = [ + $this->getRecord(Logger::DEBUG), + $this->getRecord(Logger::INFO), + ]; + $handler->handleBatch($records); + } + + public function testHtmlContent() + { + $handler = new MailerHandler($this->mailer, (new Email())->subject('Alert: %level_name% %message%')); + $handler->setFormatter(new HtmlFormatter()); + $this->mailer + ->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + return 'Alert: WARNING message' === $email->getSubject() && null === $email->getTextBody(); + })) + ; + $handler->handle($this->getRecord(Logger::WARNING, 'message')); + } + + /** + * @return array Record + */ + protected function getRecord($level = Logger::WARNING, $message = 'test', $context = []) + { + return [ + 'message' => $message, + 'context' => $context, + 'level' => $level, + 'level_name' => Logger::getLevelName($level), + 'channel' => 'test', + 'datetime' => \DateTime::createFromFormat('U.u', sprintf('%.6F', microtime(true))), + 'extra' => [], + ]; + } + + /** + * @return array + */ + protected function getMultipleRecords() + { + return [ + $this->getRecord(Logger::DEBUG, 'debug message 1'), + $this->getRecord(Logger::DEBUG, 'debug message 2'), + $this->getRecord(Logger::INFO, 'information'), + $this->getRecord(Logger::WARNING, 'warning'), + $this->getRecord(Logger::ERROR, 'error'), + ]; + } +} diff --git a/src/Symfony/Bridge/Monolog/composer.json b/src/Symfony/Bridge/Monolog/composer.json index 12680a620faa0..e3c0874f929ba 100644 --- a/src/Symfony/Bridge/Monolog/composer.json +++ b/src/Symfony/Bridge/Monolog/composer.json @@ -25,7 +25,9 @@ "symfony/console": "^4.4|^5.0", "symfony/http-client": "^4.4|^5.0", "symfony/security-core": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "symfony/var-dumper": "^4.4|^5.0", + "symfony/mailer": "^4.4|^5.0", + "symfony/mime": "^4.4|^5.0" }, "conflict": { "symfony/console": "<4.4", @@ -45,7 +47,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } } } diff --git a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md index 17c3f201bff7d..6da53d30a017e 100644 --- a/src/Symfony/Bridge/PhpUnit/CHANGELOG.md +++ b/src/Symfony/Bridge/PhpUnit/CHANGELOG.md @@ -1,6 +1,14 @@ CHANGELOG ========= +5.1.0 +----- + + * ignore verbosity settings when the build fails because of deprecations + * added per-group verbosity + * added `ExpectDeprecationTrait` to be able to define an expected deprecation from inside a test + * deprecated the `@expectedDeprecation` annotation, use the `ExpectDeprecationTrait::expectDeprecation()` method instead + 5.0.0 ----- diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php index 8ee3533064d68..72f9c7f76f954 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler.php @@ -15,6 +15,7 @@ use PHPUnit\Util\ErrorHandler; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Deprecation; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; use Symfony\Component\ErrorHandler\DebugClassLoader; /** @@ -30,24 +31,20 @@ class DeprecationErrorHandler private $mode; private $configuration; - private $deprecations = [ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - 'unsilenced' => [], - 'remaining self' => [], - 'legacy' => [], - 'other' => [], - 'remaining direct' => [], - 'remaining indirect' => [], - ]; + + /** + * @var DeprecationGroup[] + */ + private $deprecationGroups = []; private static $isRegistered = false; private static $isAtLeastPhpUnit83; + public function __construct() + { + $this->resetDeprecationGroups(); + } + /** * Registers and configures the deprecation handler. * @@ -144,9 +141,9 @@ public function handleError($type, $msg, $file, $line, $context = []) $group = 'legacy'; } else { $group = [ - Deprecation::TYPE_SELF => 'remaining self', - Deprecation::TYPE_DIRECT => 'remaining direct', - Deprecation::TYPE_INDIRECT => 'remaining indirect', + Deprecation::TYPE_SELF => 'self', + Deprecation::TYPE_DIRECT => 'direct', + Deprecation::TYPE_INDIRECT => 'indirect', Deprecation::TYPE_UNDETERMINED => 'other', ][$deprecation->getType()]; } @@ -157,18 +154,14 @@ public function handleError($type, $msg, $file, $line, $context = []) exit(1); } if ('legacy' !== $group) { - $ref = &$this->deprecations[$group][$msg]['count']; - ++$ref; - $ref = &$this->deprecations[$group][$msg][$class.'::'.$method]; - ++$ref; + $this->deprecationGroups[$group]->addNoticeFromObject($msg, $class, $method); + } else { + $this->deprecationGroups[$group]->addNotice(); } } else { - $ref = &$this->deprecations[$group][$msg]['count']; - ++$ref; + $this->deprecationGroups[$group]->addNoticeFromProceduralCode($msg); } - ++$this->deprecations[$group.'Count']; - return null; } @@ -193,34 +186,44 @@ public function shutdown() echo "\n", self::colorize('THE ERROR HANDLER HAS CHANGED!', true), "\n"; } - $groups = ['unsilenced', 'remaining self', 'remaining direct', 'remaining indirect', 'legacy', 'other']; - - $this->displayDeprecations($groups, $configuration); + $groups = array_keys($this->deprecationGroups); // store failing status - $isFailing = !$configuration->tolerates($this->deprecations); + $isFailing = !$configuration->tolerates($this->deprecationGroups); - // reset deprecations array - foreach ($this->deprecations as $group => $arrayOrInt) { - $this->deprecations[$group] = \is_int($arrayOrInt) ? 0 : []; - } + $this->displayDeprecations($groups, $configuration, $isFailing); + + $this->resetDeprecationGroups(); register_shutdown_function(function () use ($isFailing, $groups, $configuration) { - foreach ($this->deprecations as $group => $arrayOrInt) { - if (0 < (\is_int($arrayOrInt) ? $arrayOrInt : \count($arrayOrInt))) { + foreach ($this->deprecationGroups as $group) { + if ($group->count() > 0) { echo "Shutdown-time deprecations:\n"; break; } } - $this->displayDeprecations($groups, $configuration); + $isFailingAtShutdown = !$configuration->tolerates($this->deprecationGroups); + $this->displayDeprecations($groups, $configuration, $isFailingAtShutdown); - if ($isFailing || !$configuration->tolerates($this->deprecations)) { + if ($isFailing || $isFailingAtShutdown) { exit(1); } }); } + private function resetDeprecationGroups() + { + $this->deprecationGroups = [ + 'unsilenced' => new DeprecationGroup(), + 'self' => new DeprecationGroup(), + 'direct' => new DeprecationGroup(), + 'indirect' => new DeprecationGroup(), + 'legacy' => new DeprecationGroup(), + 'other' => new DeprecationGroup(), + ]; + } + private function getConfiguration() { if (null !== $this->configuration) { @@ -279,31 +282,38 @@ private static function colorize($str, $red) /** * @param string[] $groups * @param Configuration $configuration + * @param bool $isFailing */ - private function displayDeprecations($groups, $configuration) + private function displayDeprecations($groups, $configuration, $isFailing) { $cmp = function ($a, $b) { - return $b['count'] - $a['count']; + return $b->count() - $a->count(); }; foreach ($groups as $group) { - if ($this->deprecations[$group.'Count']) { + if ($this->deprecationGroups[$group]->count()) { echo "\n", self::colorize( - sprintf('%s deprecation notices (%d)', ucfirst($group), $this->deprecations[$group.'Count']), - 'legacy' !== $group && 'remaining indirect' !== $group + sprintf( + '%s deprecation notices (%d)', + \in_array($group, ['direct', 'indirect', 'self'], true) ? "Remaining $group" : ucfirst($group), + $this->deprecationGroups[$group]->count() + ), + 'legacy' !== $group && 'indirect' !== $group ), "\n"; - if (!$configuration->verboseOutput()) { + if ('legacy' !== $group && !$configuration->verboseOutput($group) && !$isFailing) { continue; } - uasort($this->deprecations[$group], $cmp); + $notices = $this->deprecationGroups[$group]->notices(); + uasort($notices, $cmp); - foreach ($this->deprecations[$group] as $msg => $notices) { - echo "\n ", $notices['count'], 'x: ', $msg, "\n"; + foreach ($notices as $msg => $notice) { + echo "\n ", $notice->count(), 'x: ', $msg, "\n"; - arsort($notices); + $countsByCaller = $notice->getCountsByCaller(); + arsort($countsByCaller); - foreach ($notices as $method => $count) { + foreach ($countsByCaller as $method => $count) { if ('count' !== $method) { echo ' ', $count, 'x in ', preg_replace('/(.*)\\\\(.*?::.*?)$/', '$2 from $1', $method), "\n"; } diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php index d26ffc45de692..bc0fe98499d41 100644 --- a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/Configuration.php @@ -32,17 +32,17 @@ class Configuration private $enabled = true; /** - * @var bool + * @var bool[] */ - private $verboseOutput = true; + private $verboseOutput; /** * @param int[] $thresholds A hash associating groups to thresholds * @param string $regex Will be matched against messages, to decide * whether to display a stack trace - * @param bool $verboseOutput + * @param bool[] $verboseOutput Keyed by groups */ - private function __construct(array $thresholds = [], $regex = '', $verboseOutput = true) + private function __construct(array $thresholds = [], $regex = '', $verboseOutput = []) { $groups = ['total', 'indirect', 'direct', 'self']; @@ -72,7 +72,21 @@ private function __construct(array $thresholds = [], $regex = '', $verboseOutput } } $this->regex = $regex; - $this->verboseOutput = $verboseOutput; + + $this->verboseOutput = [ + 'unsilenced' => true, + 'direct' => true, + 'indirect' => true, + 'self' => true, + 'other' => true, + ]; + + foreach ($verboseOutput as $group => $status) { + if (!isset($this->verboseOutput[$group])) { + throw new \InvalidArgumentException(sprintf('Unsupported verbosity group "%s", expected one of "%s".', $group, implode('", "', array_keys($this->verboseOutput)))); + } + $this->verboseOutput[$group] = (bool) $status; + } } /** @@ -84,24 +98,26 @@ public function isEnabled() } /** - * @param mixed[] $deprecations + * @param DeprecationGroup[] $deprecationGroups * * @return bool */ - public function tolerates(array $deprecations) + public function tolerates(array $deprecationGroups) { - $deprecationCounts = []; - foreach ($deprecations as $key => $deprecation) { - if (false !== strpos($key, 'Count') && false === strpos($key, 'legacy')) { - $deprecationCounts[$key] = $deprecation; + $grandTotal = 0; + + foreach ($deprecationGroups as $name => $group) { + if ('legacy' !== $name) { + $grandTotal += $group->count(); } } - if (array_sum($deprecationCounts) > $this->thresholds['total']) { + if ($grandTotal > $this->thresholds['total']) { return false; } + foreach (['self', 'direct', 'indirect'] as $deprecationType) { - if ($deprecationCounts['remaining '.$deprecationType.'Count'] > $this->thresholds[$deprecationType]) { + if ($deprecationGroups[$deprecationType]->count() > $this->thresholds[$deprecationType]) { return false; } } @@ -130,9 +146,9 @@ public function isInRegexMode() /** * @return bool */ - public function verboseOutput() + public function verboseOutput($group) { - return $this->verboseOutput; + return $this->verboseOutput[$group]; } /** @@ -145,7 +161,7 @@ public static function fromUrlEncodedString($serializedConfiguration) { parse_str($serializedConfiguration, $normalizedConfiguration); foreach (array_keys($normalizedConfiguration) as $key) { - if (!\in_array($key, ['max', 'disabled', 'verbose'], true)) { + if (!\in_array($key, ['max', 'disabled', 'verbose', 'quiet'], true)) { throw new \InvalidArgumentException(sprintf('Unknown configuration option "%s".', $key)); } } @@ -154,9 +170,19 @@ public static function fromUrlEncodedString($serializedConfiguration) return self::inDisabledMode(); } - $verboseOutput = true; - if (isset($normalizedConfiguration['verbose'])) { - $verboseOutput = (bool) $normalizedConfiguration['verbose']; + $verboseOutput = []; + if (!isset($normalizedConfiguration['verbose'])) { + $normalizedConfiguration['verbose'] = true; + } + + foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { + $verboseOutput[$group] = (bool) $normalizedConfiguration['verbose']; + } + + if (isset($normalizedConfiguration['quiet']) && \is_array($normalizedConfiguration['quiet'])) { + foreach ($normalizedConfiguration['quiet'] as $shushedGroup) { + $verboseOutput[$shushedGroup] = false; + } } return new self( @@ -190,7 +216,12 @@ public static function inStrictMode() */ public static function inWeakMode() { - return new self([], '', false); + $verboseOutput = []; + foreach (['unsilenced', 'direct', 'indirect', 'self', 'other'] as $group) { + $verboseOutput[$group] = false; + } + + return new self([], '', $verboseOutput); } /** diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php new file mode 100644 index 0000000000000..f2b0323135dd4 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationGroup.php @@ -0,0 +1,74 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; + +/** + * @internal + */ +final class DeprecationGroup +{ + private $count = 0; + + /** + * @var DeprecationNotice[] keys are messages + */ + private $deprecationNotices = []; + + /** + * @param string $message + * @param string $class + * @param string $method + */ + public function addNoticeFromObject($message, $class, $method) + { + $this->deprecationNotice($message)->addObjectOccurrence($class, $method); + $this->addNotice(); + } + + /** + * @param string $message + */ + public function addNoticeFromProceduralCode($message) + { + $this->deprecationNotice($message)->addProceduralOccurrence(); + $this->addNotice(); + } + + public function addNotice() + { + ++$this->count; + } + + /** + * @param string $message + * + * @return DeprecationNotice + */ + private function deprecationNotice($message) + { + if (!isset($this->deprecationNotices[$message])) { + $this->deprecationNotices[$message] = new DeprecationNotice(); + } + + return $this->deprecationNotices[$message]; + } + + public function count() + { + return $this->count; + } + + public function notices() + { + return $this->deprecationNotices; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php new file mode 100644 index 0000000000000..854bbd4d26333 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/DeprecationErrorHandler/DeprecationNotice.php @@ -0,0 +1,49 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\DeprecationErrorHandler; + +/** + * @internal + */ +final class DeprecationNotice +{ + private $count = 0; + + /** + * @var int[] + */ + private $countsByCaller = []; + + public function addObjectOccurrence($class, $method) + { + if (!isset($this->countsByCaller["$class::$method"])) { + $this->countsByCaller["$class::$method"] = 0; + } + ++$this->countsByCaller["$class::$method"]; + ++$this->count; + } + + public function addProceduralOccurrence() + { + ++$this->count; + } + + public function getCountsByCaller() + { + return $this->countsByCaller; + } + + public function count() + { + return $this->count; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php b/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php new file mode 100644 index 0000000000000..0db391d12abab --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/ExpectDeprecationTrait.php @@ -0,0 +1,31 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit; + +use Symfony\Bridge\PhpUnit\Legacy\SymfonyTestsListenerTrait; + +trait ExpectDeprecationTrait +{ + /** + * @param string $message + * + * @return void + */ + protected function expectDeprecation($message) + { + if (!SymfonyTestsListenerTrait::$previousErrorHandler) { + SymfonyTestsListenerTrait::$previousErrorHandler = set_error_handler([SymfonyTestsListenerTrait::class, 'handleError']); + } + + SymfonyTestsListenerTrait::$expectedDeprecations[] = $message; + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php index 7a64ed0d91cd5..2bc6de0705878 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/CoverageListenerTrait.php @@ -69,10 +69,19 @@ public function startTest($test) $r = new \ReflectionProperty(Test::class, 'annotationCache'); $r->setAccessible(true); + $covers = $sutFqcn; + if (!\is_array($sutFqcn)) { + $covers = [$sutFqcn]; + while ($parent = get_parent_class($sutFqcn)) { + $covers[] = $parent; + $sutFqcn = $parent; + } + } + $cache = $r->getValue(); $cache = array_replace_recursive($cache, [ \get_class($test) => [ - 'covers' => \is_array($sutFqcn) ? $sutFqcn : [$sutFqcn], + 'covers' => $covers, ], ]); $r->setValue(Test::class, $cache); diff --git a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php index f9a1cadb49652..00433b16b7ee3 100644 --- a/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php +++ b/src/Symfony/Bridge/PhpUnit/Legacy/SymfonyTestsListenerTrait.php @@ -13,6 +13,7 @@ use Doctrine\Common\Annotations\AnnotationRegistry; use PHPUnit\Framework\AssertionFailedError; +use PHPUnit\Framework\RiskyTestError; use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestSuite; use PHPUnit\Runner\BaseTestRunner; @@ -20,6 +21,7 @@ use PHPUnit\Util\Test; use Symfony\Bridge\PhpUnit\ClockMock; use Symfony\Bridge\PhpUnit\DnsMock; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; use Symfony\Component\Debug\DebugClassLoader as LegacyDebugClassLoader; use Symfony\Component\ErrorHandler\DebugClassLoader; @@ -32,16 +34,16 @@ */ class SymfonyTestsListenerTrait { + public static $expectedDeprecations = []; + public static $previousErrorHandler; + private static $gatheredDeprecations = []; private static $globallyEnabled = false; private $state = -1; private $skippedFile = false; private $wasSkipped = []; private $isSkipped = []; - private $expectedDeprecations = []; - private $gatheredDeprecations = []; - private $previousErrorHandler; - private $error; private $runsInSeparateProcess = false; + private $checkNumAssertions = false; /** * @param array $mockedNamespaces List of namespaces, indexed by mocked features (time-sensitive or dns-sensitive) @@ -225,15 +227,18 @@ public function startTest($test) if (isset($annotations['class']['expectedDeprecation'])) { $test->getTestResultObject()->addError($test, new AssertionFailedError('`@expectedDeprecation` annotations are not allowed at the class level.'), 0); } - if (isset($annotations['method']['expectedDeprecation'])) { - if (!\in_array('legacy', $groups, true)) { - $this->error = new AssertionFailedError('Only tests with the `@group legacy` annotation can have `@expectedDeprecation`.'); + if (isset($annotations['method']['expectedDeprecation']) || $this->checkNumAssertions = \in_array(ExpectDeprecationTrait::class, class_uses($test), true)) { + if (isset($annotations['method']['expectedDeprecation'])) { + self::$expectedDeprecations = $annotations['method']['expectedDeprecation']; + self::$previousErrorHandler = set_error_handler([self::class, 'handleError']); + @trigger_error('Since symfony/phpunit-bridge 5.1: Using "@expectedDeprecation" annotations in tests is deprecated, use the "ExpectDeprecationTrait::expectDeprecation()" method instead.', E_USER_DEPRECATED); } - $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); + if ($this->checkNumAssertions) { + $this->checkNumAssertions = $test->getTestResultObject()->isStrictAboutTestsThatDoNotTestAnything() && !$test->doesNotPerformAssertions(); + } - $this->expectedDeprecations = $annotations['method']['expectedDeprecation']; - $this->previousErrorHandler = set_error_handler([$this, 'handleError']); + $test->getTestResultObject()->beStrictAboutTestsThatDoNotTestAnything(false); } } } @@ -247,9 +252,12 @@ public function endTest($test, $time) $className = \get_class($test); $groups = Test::getGroups($className, $test->getName(false)); - if ($errored = null !== $this->error) { - $test->getTestResultObject()->addError($test, $this->error, 0); - $this->error = null; + if ($this->checkNumAssertions) { + if (!self::$expectedDeprecations && !$test->getNumAssertions() && $test->getTestResultObject()->noneSkipped()) { + $test->getTestResultObject()->addFailure($test, new RiskyTestError('This test did not perform any assertions'), $time); + } + + $this->checkNumAssertions = false; } if ($this->runsInSeparateProcess) { @@ -268,24 +276,26 @@ public function endTest($test, $time) $this->runsInSeparateProcess = false; } - if ($this->expectedDeprecations) { + if (self::$expectedDeprecations) { if (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE], true)) { - $test->addToAssertionCount(\count($this->expectedDeprecations)); + $test->addToAssertionCount(\count(self::$expectedDeprecations)); } restore_error_handler(); - if (!$errored && !\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { + if (!\in_array('legacy', $groups, true)) { + $test->getTestResultObject()->addError($test, new AssertionFailedError('Only tests with the `@group legacy` annotation can expect a deprecation.'), 0); + } elseif (!\in_array($test->getStatus(), [BaseTestRunner::STATUS_SKIPPED, BaseTestRunner::STATUS_INCOMPLETE, BaseTestRunner::STATUS_FAILURE, BaseTestRunner::STATUS_ERROR], true)) { try { $prefix = "@expectedDeprecation:\n"; - $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", $this->expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", $this->gatheredDeprecations)."\n"); + $test->assertStringMatchesFormat($prefix.'%A '.implode("\n%A ", self::$expectedDeprecations)."\n%A", $prefix.' '.implode("\n ", self::$gatheredDeprecations)."\n"); } catch (AssertionFailedError $e) { $test->getTestResultObject()->addFailure($test, $e, $time); } } - $this->expectedDeprecations = $this->gatheredDeprecations = []; - $this->previousErrorHandler = null; + self::$expectedDeprecations = self::$gatheredDeprecations = []; + self::$previousErrorHandler = null; } if (!$this->runsInSeparateProcess && -2 < $this->state && ($test instanceof \PHPUnit\Framework\TestCase || $test instanceof TestCase)) { if (\in_array('time-sensitive', $groups, true)) { @@ -297,10 +307,10 @@ public function endTest($test, $time) } } - public function handleError($type, $msg, $file, $line, $context = []) + public static function handleError($type, $msg, $file, $line, $context = []) { if (E_USER_DEPRECATED !== $type && E_DEPRECATED !== $type) { - $h = $this->previousErrorHandler; + $h = self::$previousErrorHandler; return $h ? $h($type, $msg, $file, $line, $context) : false; } @@ -313,7 +323,7 @@ public function handleError($type, $msg, $file, $line, $context = []) if (error_reporting()) { $msg = 'Unsilenced deprecation: '.$msg; } - $this->gatheredDeprecations[] = $msg; + self::$gatheredDeprecations[] = $msg; return null; } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php index 39e792cd3a2cb..bb5b3a72d4932 100644 --- a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/ConfigurationTest.php @@ -13,6 +13,7 @@ use PHPUnit\Framework\TestCase; use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\Configuration; +use Symfony\Bridge\PhpUnit\DeprecationErrorHandler\DeprecationGroup; class ConfigurationTest extends TestCase { @@ -47,122 +48,122 @@ public function testItThrowsOnStringishThreshold() public function testItNoticesExceededTotalThreshold() { $configuration = Configuration::fromUrlEncodedString('max[total]=3'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1, - 'remaining selfCount' => 0, - 'legacyCount' => 1, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 1, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1, - 'remaining selfCount' => 1, - 'legacyCount' => 1, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 1, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1, + 'self' => 0, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1, + 'self' => 1, + 'legacy' => 1, + 'other' => 0, + 'direct' => 1, + 'indirect' => 1, + ]))); } public function testItNoticesExceededSelfThreshold() { $configuration = Configuration::fromUrlEncodedString('max[self]=1'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 1, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 124, - 'remaining indirectCount' => 3244, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 2, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 124, - 'remaining indirectCount' => 3244, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 1, + 'legacy' => 23, + 'other' => 13, + 'direct' => 124, + 'indirect' => 3244, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 2, + 'legacy' => 23, + 'other' => 13, + 'direct' => 124, + 'indirect' => 3244, + ]))); } public function testItNoticesExceededDirectThreshold() { $configuration = Configuration::fromUrlEncodedString('max[direct]=1&max[self]=999999'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 123, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 1, - 'remaining indirectCount' => 3244, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 124, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 2, - 'remaining indirectCount' => 3244, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 123, + 'legacy' => 23, + 'other' => 13, + 'direct' => 1, + 'indirect' => 3244, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 124, + 'legacy' => 23, + 'other' => 13, + 'direct' => 2, + 'indirect' => 3244, + ]))); } public function testItNoticesExceededIndirectThreshold() { $configuration = Configuration::fromUrlEncodedString('max[indirect]=1&max[direct]=999999&max[self]=999999'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 123, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 1234, - 'remaining indirectCount' => 1, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 1234, - 'remaining selfCount' => 124, - 'legacyCount' => 23, - 'otherCount' => 13, - 'remaining directCount' => 2324, - 'remaining indirectCount' => 2, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 123, + 'legacy' => 23, + 'other' => 13, + 'direct' => 1234, + 'indirect' => 1, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 1234, + 'self' => 124, + 'legacy' => 23, + 'other' => 13, + 'direct' => 2324, + 'indirect' => 2, + ]))); } public function testIndirectThresholdIsUsedAsADefaultForDirectAndSelfThreshold() { $configuration = Configuration::fromUrlEncodedString('max[indirect]=1'); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 1, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 2, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 0, - 'remaining indirectCount' => 0, - ])); - $this->assertTrue($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 1, - 'remaining indirectCount' => 0, - ])); - $this->assertFalse($configuration->tolerates([ - 'unsilencedCount' => 0, - 'remaining selfCount' => 0, - 'legacyCount' => 0, - 'otherCount' => 0, - 'remaining directCount' => 2, - 'remaining indirectCount' => 0, - ])); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 1, + 'legacy' => 0, + 'other' => 0, + 'direct' => 0, + 'indirect' => 0, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 2, + 'legacy' => 0, + 'other' => 0, + 'direct' => 0, + 'indirect' => 0, + ]))); + $this->assertTrue($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 0, + 'other' => 0, + 'direct' => 1, + 'indirect' => 0, + ]))); + $this->assertFalse($configuration->tolerates($this->buildGroups([ + 'unsilenced' => 0, + 'self' => 0, + 'legacy' => 0, + 'other' => 0, + 'direct' => 2, + 'indirect' => 0, + ]))); } public function testItCanTellWhetherToDisplayAStackTrace() @@ -184,12 +185,51 @@ public function testItCanBeDisabled() public function testItCanBeShushed() { $configuration = Configuration::fromUrlEncodedString('verbose'); - $this->assertFalse($configuration->verboseOutput()); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertFalse($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertFalse($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + public function testItCanBePartiallyShushed() + { + $configuration = Configuration::fromUrlEncodedString('quiet[]=unsilenced&quiet[]=indirect&quiet[]=other'); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertTrue($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertTrue($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + public function testItThrowsOnUnknownVerbosityGroup() + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('made-up'); + Configuration::fromUrlEncodedString('quiet[]=made-up'); } public function testOutputIsNotVerboseInWeakMode() { $configuration = Configuration::inWeakMode(); - $this->assertFalse($configuration->verboseOutput()); + $this->assertFalse($configuration->verboseOutput('unsilenced')); + $this->assertFalse($configuration->verboseOutput('direct')); + $this->assertFalse($configuration->verboseOutput('indirect')); + $this->assertFalse($configuration->verboseOutput('self')); + $this->assertFalse($configuration->verboseOutput('other')); + } + + private function buildGroups($counts) + { + $groups = []; + foreach ($counts as $name => $count) { + $groups[$name] = new DeprecationGroup(); + $i = 0; + while ($i++ < $count) { + $groups[$name]->addNotice(); + } + } + + return $groups; } } diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php new file mode 100644 index 0000000000000..df746e5e38907 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationGroupTest.php @@ -0,0 +1,30 @@ +addNoticeFromObject( + 'Calling sfContext::getInstance() is deprecated', + 'MonsterController', + 'get5klocMethod' + ); + $group->addNoticeFromProceduralCode('Calling sfContext::getInstance() is deprecated'); + $this->assertCount(1, $group->notices()); + $this->assertSame(2, $group->count()); + } + + public function testItAllowsAddingANoticeWithoutClutteringTheMemory() + { + // this is useful for notices in the legacy group + $group = new DeprecationGroup(); + $group->addNotice(); + $this->assertSame(1, $group->count()); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php new file mode 100644 index 0000000000000..c0a88c443b4d7 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/DeprecationNoticeTest.php @@ -0,0 +1,35 @@ +addObjectOccurrence('MyAction', '__invoke'); + $notice->addObjectOccurrence('MyAction', '__invoke'); + $notice->addObjectOccurrence('MyOtherAction', '__invoke'); + + $countsByCaller = $notice->getCountsByCaller(); + + $this->assertCount(2, $countsByCaller); + $this->assertArrayHasKey('MyAction::__invoke', $countsByCaller); + $this->assertArrayHasKey('MyOtherAction::__invoke', $countsByCaller); + $this->assertSame(2, $countsByCaller['MyAction::__invoke']); + $this->assertSame(1, $countsByCaller['MyOtherAction::__invoke']); + } + + public function testItCountsBothTypesOfOccurrences() + { + $notice = new DeprecationNotice(); + $notice->addObjectOccurrence('MyAction', '__invoke'); + $this->assertSame(1, $notice->count()); + + $notice->addProceduralOccurrence(); + $this->assertSame(2, $notice->count()); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt new file mode 100644 index 0000000000000..d45c6f9af2687 --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/partially_quiet.phpt @@ -0,0 +1,37 @@ +--TEST-- +Test DeprecationErrorHandler quiet on everything but indirect deprecations +--FILE-- + +--EXPECTF-- +Unsilenced deprecation notices (3) + +Remaining direct deprecation notices (1) + +Remaining indirect deprecation notices (1) + + 1x: deprecatedApi is deprecated! You should stop relying on it! + 1x in SomeService::deprecatedApi from acme\lib + +Legacy deprecation notices (2) + +Other deprecation notices (1) + diff --git a/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt new file mode 100644 index 0000000000000..9c73d3c4430ae --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/DeprecationErrorHandler/quiet_but_failing.phpt @@ -0,0 +1,39 @@ +--TEST-- +Test DeprecationErrorHandler when failing and not verbose +--FILE-- + +--EXPECTF-- +Remaining indirect deprecation notices (1) + + 1x: deprecatedApi is deprecated! You should stop relying on it! + 1x in SomeService::deprecatedApi from acme\lib diff --git a/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php b/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php new file mode 100644 index 0000000000000..2d3f0e7a8b79f --- /dev/null +++ b/src/Symfony/Bridge/PhpUnit/Tests/ExpectDeprecationTraitTest.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bridge\PhpUnit\Tests; + +use PHPUnit\Framework\TestCase; +use Symfony\Bridge\PhpUnit\ExpectDeprecationTrait; + +final class ExpectDeprecationTraitTest extends TestCase +{ + use ExpectDeprecationTrait; + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testOne() + { + $this->expectDeprecation('foo'); + @trigger_error('foo', E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + */ + public function testMany() + { + $this->expectDeprecation('foo'); + $this->expectDeprecation('bar'); + @trigger_error('foo', E_USER_DEPRECATED); + @trigger_error('bar', E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * + * @expectedDeprecation foo + */ + public function testOneWithAnnotation() + { + $this->expectDeprecation('bar'); + @trigger_error('foo', E_USER_DEPRECATED); + @trigger_error('bar', E_USER_DEPRECATED); + } + + /** + * Do not remove this test in the next major version. + * + * @group legacy + * + * @expectedDeprecation foo + * @expectedDeprecation bar + */ + public function testManyWithAnnotation() + { + $this->expectDeprecation('ccc'); + $this->expectDeprecation('fcy'); + @trigger_error('foo', E_USER_DEPRECATED); + @trigger_error('bar', E_USER_DEPRECATED); + @trigger_error('ccc', E_USER_DEPRECATED); + @trigger_error('fcy', E_USER_DEPRECATED); + } +} diff --git a/src/Symfony/Bridge/PhpUnit/composer.json b/src/Symfony/Bridge/PhpUnit/composer.json index 15280797e2dc5..a285a435ce9bd 100644 --- a/src/Symfony/Bridge/PhpUnit/composer.json +++ b/src/Symfony/Bridge/PhpUnit/composer.json @@ -39,7 +39,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" }, "thanks": { "name": "phpunit/phpunit", diff --git a/src/Symfony/Bridge/ProxyManager/composer.json b/src/Symfony/Bridge/ProxyManager/composer.json index 980d21f4303db..99effe166c816 100644 --- a/src/Symfony/Bridge/ProxyManager/composer.json +++ b/src/Symfony/Bridge/ProxyManager/composer.json @@ -35,7 +35,7 @@ "minimum-stability": "dev", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-master": "5.1-dev" } } } diff --git a/src/Symfony/Bridge/Twig/CHANGELOG.md b/src/Symfony/Bridge/Twig/CHANGELOG.md index c70801a89014c..70ca5e7481691 100644 --- a/src/Symfony/Bridge/Twig/CHANGELOG.md +++ b/src/Symfony/Bridge/Twig/CHANGELOG.md @@ -8,6 +8,8 @@ CHANGELOG * removed `transChoice` filter and token * `HttpFoundationExtension` requires a `UrlHelper` on instantiation * removed support for implicit STDIN usage in the `lint:twig` command, use `lint:twig -` (append a dash) instead to make it explicit. + * added form theme for Foundation 6 + * added support for Foundation 6 switches: add the `switch-input` class to the attributes of a `CheckboxType` 4.4.0 ----- diff --git a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php index 5a79062d362a1..b0ccd684e8b6f 100644 --- a/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php +++ b/src/Symfony/Bridge/Twig/ErrorRenderer/TwigErrorRenderer.php @@ -35,7 +35,7 @@ class TwigErrorRenderer implements ErrorRendererInterface public function __construct(Environment $twig, HtmlErrorRenderer $fallbackErrorRenderer = null, $debug = false) { if (!\is_bool($debug) && !\is_callable($debug)) { - throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, \is_object($debug) ? \get_class($debug) : \gettype($debug))); + throw new \TypeError(sprintf('Argument 3 passed to "%s()" must be a boolean or a callable, "%s" given.', __METHOD__, get_debug_type($debug))); } $this->twig = $twig; diff --git a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php index e1031b3d569c2..14535f232a0a4 100644 --- a/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php +++ b/src/Symfony/Bridge/Twig/Mime/BodyRenderer.php @@ -47,7 +47,7 @@ public function render(Message $message): void $messageContext = $message->getContext(); if (isset($messageContext['email'])) { - throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', \get_class($message))); + throw new InvalidArgumentException(sprintf('A "%s" context cannot have an "email" entry as this is a reserved variable.', get_debug_type($message))); } $vars = array_merge($this->context, $messageContext, [ diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig index 44492cebe74d7..9c8a0cc14f38d 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig @@ -99,7 +99,7 @@ {%- endif -%} {%- endif -%} - {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? label : label|trans(label_translation_parameters, translation_domain)) -}} + {{- widget|raw }} {{ label is not same as(false) ? (translation_domain is same as(false) ? (label_html is same as(false) ? label : label|raw) : (label_html is same as(false) ? label|trans(label_translation_parameters, translation_domain) : label|trans(label_translation_parameters, translation_domain)|raw)) -}} {%- endif -%} {%- endblock checkbox_radio_label %} diff --git a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig index 8ac32978a0925..86e2715488338 100644 --- a/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig +++ b/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_4_layout.html.twig @@ -123,7 +123,9 @@ {%- set type = type|default('file') -%} {{- block('form_widget_simple') -}} {%- set label_attr = label_attr|merge({ class: (label_attr.class|default('') ~ ' custom-file-label')|trim }) -%} -