diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md
index 90e51d60536d6..3d21822287b6b 100644
--- a/.github/PULL_REQUEST_TEMPLATE.md
+++ b/.github/PULL_REQUEST_TEMPLATE.md
@@ -1,6 +1,6 @@
| Q | A
| ------------- | ---
-| Branch? | 7.3 for features / 5.4, 6.4, 7.1, and 7.2 for bug fixes
+| Branch? | 7.3 for features / 6.4, 7.1, and 7.2 for bug fixes
| Bug fix? | yes/no
| New feature? | yes/no
| Deprecations? | yes/no
diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml
index dfc5b0e63728f..c72f7e5e34cd6 100644
--- a/.github/workflows/unit-tests.yml
+++ b/.github/workflows/unit-tests.yml
@@ -33,6 +33,7 @@ jobs:
mode: low-deps
- php: '8.3'
- php: '8.4'
+ - php: '8.5'
#mode: experimental
fail-fast: false
diff --git a/CHANGELOG-7.1.md b/CHANGELOG-7.1.md
index 4950ff8986131..f46dc88b01503 100644
--- a/CHANGELOG-7.1.md
+++ b/CHANGELOG-7.1.md
@@ -7,6 +7,28 @@ in 7.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/v7.1.0...v7.1.1
+* 7.1.10 (2024-12-31)
+
+ * bug #59304 [PropertyInfo] Remove ``@internal`` from `PropertyReadInfo` and `PropertyWriteInfo` (Dario Guarracino)
+ * bug #59228 [HttpFoundation] Avoid mime type guess with temp files in `BinaryFileResponse` (alexandre-daubois)
+ * bug #59318 [Finder] Fix using `==` as default operator in `DateComparator` (MatTheCat)
+ * bug #59321 [HtmlSanitizer] reject URLs containing whitespaces (xabbuh)
+ * bug #59250 [HttpClient] Fix a typo in NoPrivateNetworkHttpClient (Jontsa)
+ * bug #59103 [Messenger] ensure exception on rollback does not hide previous exception (nikophil)
+ * bug #59226 [FrameworkBundle] require the writer to implement getFormats() in the translation:extract (xabbuh)
+ * bug #59213 [FrameworkBundle] don't require fake notifier transports to be installed as non-dev dependencies (xabbuh)
+ * bug #59066 Fix resolve enum in string type resolver (DavidBadura)
+ * bug #59156 [PropertyInfo] Fix interface handling in PhpStanTypeHelper (mtarld)
+ * bug #59160 [BeanstalkMessenger] Round delay to an integer to avoid deprecation warning (plantas)
+ * bug #59012 [PropertyInfo] Fix interface handling in `PhpStanTypeHelper` (janedbal)
+ * bug #59134 [HttpKernel] Denormalize request data using the csv format when using "#[MapQueryString]" or "#[MapRequestPayload]" (except for content data) (ovidiuenache)
+ * bug #59140 [WebProfilerBundle] fix: white-space in highlighted code (chr-hertel)
+ * bug #59124 [FrameworkBundle] fix: notifier push channel bus abstract arg (raphael-geffroy)
+ * bug #59069 [Console] Fix division by 0 error (Rindula)
+ * bug #59070 [PropertyInfo] evaluate access flags for properties with asymmetric visibility (xabbuh)
+ * bug #59062 [HttpClient] Always set CURLOPT_CUSTOMREQUEST to the correct HTTP method in CurlHttpClient (KurtThiemann)
+ * bug #59023 [HttpClient] Fix streaming and redirecting with NoPrivateNetworkHttpClient (nicolas-grekas)
+
* 7.1.9 (2024-11-27)
* bug #59013 [HttpClient] Fix checking for private IPs before connecting (nicolas-grekas)
diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md
index bcc33dc4892f2..c83c2ca56b1d4 100644
--- a/CONTRIBUTORS.md
+++ b/CONTRIBUTORS.md
@@ -19,8 +19,8 @@ The Symfony Connect username in parenthesis allows to get more information
- Jordi Boggiano (seldaek)
- Maxime Steinhausser (ogizanagi)
- Kévin Dunglas (dunglas)
- - Victor Berchet (victor)
- Javier Eguiluz (javier.eguiluz)
+ - Victor Berchet (victor)
- Ryan Weaver (weaverryan)
- Jérémy DERUSSÉ (jderusse)
- Jules Pietri (heah)
@@ -51,15 +51,15 @@ The Symfony Connect username in parenthesis allows to get more information
- Igor Wiedler
- Jan Schädlich (jschaedl)
- Mathieu Lechat (mat_the_cat)
+ - Simon André (simonandre)
- Matthias Pigulla (mpdude)
- Gabriel Ostrolucký (gadelat)
- - Simon André (simonandre)
- Jonathan Wage (jwage)
+ - Mathias Arlaud (mtarld)
- Vincent Langlet (deviling)
- Valentin Udaltsov (vudaltsov)
- - Mathias Arlaud (mtarld)
- - Alexandre Salomé (alexandresalome)
- Grégoire Paris (greg0ire)
+ - Alexandre Salomé (alexandresalome)
- William DURAND
- ornicar
- Dany Maillard (maidmaid)
@@ -83,11 +83,11 @@ The Symfony Connect username in parenthesis allows to get more information
- Alexander Schranz (alexander-schranz)
- Mathieu Piot (mpiot)
- Vasilij Duško (staff)
+ - Dariusz Ruminski
- Sarah Khalil (saro0h)
- Laurent VOULLEMIER (lvo)
- Konstantin Kudryashov (everzet)
- Guilhem N (guilhemn)
- - Dariusz Ruminski
- Bilal Amarni (bamarni)
- Eriksen Costa
- Florin Patan (florinpatan)
@@ -110,12 +110,12 @@ The Symfony Connect username in parenthesis allows to get more information
- Baldini
- Alex Pott
- Fran Moreno (franmomu)
+ - Hubert Lenoir (hubert_lenoir)
- Charles Sarrazin (csarrazi)
- Henrik Westphal (snc)
- Dariusz Górecki (canni)
- - Hubert Lenoir (hubert_lenoir)
- - Ener-Getick
- Antoine Makdessi (amakdessi)
+ - Ener-Getick
- Graham Campbell (graham)
- Tugdual Saunier (tucksaun)
- Lee McDermott
@@ -148,6 +148,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jérôme Vasseur (jvasseur)
- Peter Kokot (peterkokot)
- Brice BERNARD (brikou)
+ - Valtteri R (valtzu)
- Martin Auswöger
- Michal Piotrowski
- marc.weistroff
@@ -156,7 +157,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Vladimir Tsykun (vtsykun)
- Jacob Dreesen (jdreesen)
- Włodzimierz Gajda (gajdaw)
- - Valtteri R (valtzu)
- Nicolas Philippe (nikophil)
- Javier Spagnoletti (phansys)
- Adrien Brault (adrienbrault)
@@ -170,6 +170,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Baptiste Clavié (talus)
- Alexander Schwenn (xelaris)
- Fabien Pennequin (fabienpennequin)
+ - Dāvis Zālītis (k0d3r1s)
- Gordon Franke (gimler)
- Malte Schlüter (maltemaltesich)
- jeremyFreeAgent (jeremyfreeagent)
@@ -178,7 +179,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Vasilij Dusko
- Daniel Wehner (dawehner)
- Maxime Helias (maxhelias)
- - Dāvis Zālītis (k0d3r1s)
- Robert Schönthal (digitalkaoz)
- Smaine Milianni (ismail1432)
- François-Xavier de Guillebon (de-gui_f)
@@ -193,6 +193,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jhonny Lidfors (jhonne)
- Juti Noppornpitak (shiroyuki)
- Gregor Harlan (gharlan)
+ - Alexis Lefebvre
- Hugo Alliaume (kocal)
- Anthony MARTIN
- Sebastian Hörl (blogsh)
@@ -206,7 +207,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Guilherme Blanco (guilhermeblanco)
- Saif Eddin Gmati (azjezz)
- Farhad Safarov (safarov)
- - Alexis Lefebvre
- SpacePossum
- Richard van Laak (rvanlaak)
- Andreas Braun
@@ -351,6 +351,7 @@ The Symfony Connect username in parenthesis allows to get more information
- fd6130 (fdtvui)
- Priyadi Iman Nurcahyo (priyadi)
- Alan Poulain (alanpoulain)
+ - Oleg Andreyev (oleg.andreyev)
- Maciej Malarz (malarzm)
- Marcin Sikoń (marphi)
- Michele Orselli (orso)
@@ -390,13 +391,13 @@ The Symfony Connect username in parenthesis allows to get more information
- Alexander Kotynia (olden)
- Elnur Abdurrakhimov (elnur)
- Manuel Reinhard (sprain)
+ - Zan Baldwin (zanbaldwin)
- Antonio J. García Lagar (ajgarlag)
- BoShurik
- Quentin Devos
- Adam Prager (padam87)
- Benoît Burnichon (bburnichon)
- maxime.steinhausser
- - Oleg Andreyev (oleg.andreyev)
- Roman Ring (inori)
- Xavier Montaña Carreras (xmontana)
- Arjen van der Meijden
@@ -460,7 +461,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Magnus Nordlander (magnusnordlander)
- Tim Goudriaan (codedmonkey)
- Robert Kiss (kepten)
- - Zan Baldwin (zanbaldwin)
- Alexandre Quercia (alquerci)
- Marcos Sánchez
- Emanuele Panzeri (thepanz)
@@ -484,6 +484,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Bohan Yang (brentybh)
- Vilius Grigaliūnas
- David Badura (davidbadura)
+ - Jordane VASPARD (elementaire)
- Chris Smith (cs278)
- Thomas Bisignani (toma)
- Florian Klein (docteurklein)
@@ -582,7 +583,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Alexander Menshchikov
- Clément Gautier (clementgautier)
- roman joly (eltharin)
- - Jordane VASPARD (elementaire)
- James Gilliland (neclimdul)
- Sanpi (sanpi)
- Eduardo Gulias (egulias)
@@ -683,6 +683,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Neil Peyssard (nepey)
- Niklas Fiekas
- Mark Challoner (markchalloner)
+ - Andreas Hennings
- Markus Bachmann (baachi)
- Gunnstein Lye (glye)
- Erkhembayar Gantulga (erheme318)
@@ -797,6 +798,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kev
- Kevin McBride
- Sergio Santoro
+ - Jonas Elfering
- Philipp Rieber (bicpi)
- Dmitriy Derepko
- Manuel de Ruiter (manuel)
@@ -949,7 +951,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Franck RANAIVO-HARISOA (franckranaivo)
- Yi-Jyun Pan
- Egor Taranov
- - Andreas Hennings
- Arnaud Frézet
- Philippe Segatori
- Jon Gotlin (jongotlin)
@@ -1295,6 +1296,7 @@ The Symfony Connect username in parenthesis allows to get more information
- _sir_kane (waly)
- Olivier Maisonneuve
- Gálik Pál
+ - Bálint Szekeres
- Andrei C. (moldman)
- Mike Meier (mykon)
- Pedro Miguel Maymone de Resende (pedroresende)
@@ -1306,6 +1308,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Kagan Balga (kagan-balga)
- Nikita Nefedov (nikita2206)
- Alex Bacart
+ - StefanoTarditi
- cgonzalez
- hugovms
- Ben
@@ -1418,6 +1421,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jason Woods
- mwsaz
- bogdan
+ - wanxiangchwng
- Geert De Deckere
- grizlik
- Derek ROTH
@@ -1447,7 +1451,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Morten Wulff (wulff)
- Kieran
- Don Pinkster
- - Jonas Elfering
- Maksim Muruev
- Emil Einarsson
- 243083df
@@ -1624,6 +1627,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Luciano Mammino (loige)
- LHommet Nicolas (nicolaslh)
- fabios
+ - eRIZ
- Sander Coolen (scoolen)
- Vic D'Elfant (vicdelfant)
- Amirreza Shafaat (amirrezashafaat)
@@ -2034,6 +2038,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Vladimir Mantulo (mantulo)
- Boullé William (williamboulle)
- Jesper Noordsij
+ - Bart Baaten
- Frederic Godfrin
- Paul Matthews
- aim8604
@@ -2068,6 +2073,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Dalibor Karlović
- Cesar Scur (cesarscur)
- Cyril Vermandé (cyve)
+ - Daniele Orru' (danydev)
- Raul Garcia Canet (juagarc4)
- Sagrario Meneses
- Dmitri Petmanson
@@ -2161,6 +2167,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Maxime THIRY
- Norman Soetbeer
- Ludek Stepan
+ - Benjamin BOUDIER
- Frederik Schwan
- Mark van den Berg
- Aaron Stephens (astephens)
@@ -2276,6 +2283,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Frank Neff (fneff)
- Volodymyr Kupriienko (greeflas)
- Ilya Biryukov (ibiryukov)
+ - Mathieu Ledru (matyo91)
- Roma (memphys)
- Florian Caron (shalalalala)
- Serhiy Lunak (slunak)
@@ -2381,7 +2389,6 @@ The Symfony Connect username in parenthesis allows to get more information
- Nicolas Eeckeloo (neeckeloo)
- Andriy Prokopenko (sleepyboy)
- Dariusz Ruminski
- - Bálint Szekeres
- Starfox64
- Ivo Valchev
- Thomas Hanke
@@ -2472,6 +2479,7 @@ The Symfony Connect username in parenthesis allows to get more information
- karstennilsen
- kaywalker
- Sebastian Ionescu
+ - Kurt Thiemann
- Robert Kopera
- Pablo Ogando Ferreira
- Thomas Ploch
@@ -2481,6 +2489,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Jeremiah VALERIE
- Alexandre Beaujour
- Franck Ranaivo-Harisoa
+ - Grégoire Rabasse
- Cas van Dongen
- Patrik Patie Gmitter
- George Yiannoulopoulos
@@ -2560,6 +2569,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Tobias Genberg (lorceroth)
- Michael Simonson (mikes)
- Nicolas Badey (nico-b)
+ - Florent Blaison (orkin)
- Olivier Scherler (oscherler)
- Flo Gleixner (redflo)
- Romain Jacquart (romainjacquart)
@@ -3158,6 +3168,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Vlad Dumitrache
- wetternest
- Erik van Wingerden
+ - matlec
- Valouleloup
- Pathpat
- Jaymin G
@@ -3302,6 +3313,7 @@ The Symfony Connect username in parenthesis allows to get more information
- dasmfm
- Claas Augner
- Mathias Geat
+ - neodevcode
- Angel Fernando Quiroz Campos (angelfqc)
- Arnaud Buathier (arnapou)
- Curtis (ccorliss)
@@ -3362,6 +3374,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Steffen Keuper
- Kai Eichinger
- Antonio Angelino
+ - Jan Nedbal
- Jens Schulze
- Tema Yud
- Matt Fields
@@ -3393,6 +3406,7 @@ The Symfony Connect username in parenthesis allows to get more information
- Menno Holtkamp
- Ser5
- Michael Hudson-Doyle
+ - Matthew Burns
- Daniel Bannert
- Karim Miladi
- Michael Genereux
@@ -3771,6 +3785,7 @@ The Symfony Connect username in parenthesis allows to get more information
- damaya
- Kevin Weber
- Alexandru Năstase
+ - Carl Julian Sauter
- Dionysis Arvanitis
- Sergey Fedotov
- Konstantin Scheumann
diff --git a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
index e4831557f01db..8e10891b0ba74 100644
--- a/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
+++ b/src/Symfony/Bridge/Doctrine/Messenger/DoctrineTransactionMiddleware.php
@@ -27,15 +27,17 @@ class DoctrineTransactionMiddleware extends AbstractDoctrineMiddleware
protected function handleForManager(EntityManagerInterface $entityManager, Envelope $envelope, StackInterface $stack): Envelope
{
$entityManager->getConnection()->beginTransaction();
+
+ $success = false;
try {
$envelope = $stack->next()->handle($envelope, $stack);
$entityManager->flush();
$entityManager->getConnection()->commit();
+ $success = true;
+
return $envelope;
} catch (\Throwable $exception) {
- $entityManager->getConnection()->rollBack();
-
if ($exception instanceof HandlerFailedException) {
// Remove all HandledStamp from the envelope so the retry will execute all handlers again.
// When a handler fails, the queries of allegedly successful previous handlers just got rolled back.
@@ -43,6 +45,12 @@ protected function handleForManager(EntityManagerInterface $entityManager, Envel
}
throw $exception;
+ } finally {
+ $connection = $entityManager->getConnection();
+
+ if (!$success && $connection->isTransactionActive()) {
+ $connection->rollBack();
+ }
}
}
}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php
new file mode 100644
index 0000000000000..d6f82f8214846
--- /dev/null
+++ b/src/Symfony/Bridge/Doctrine/Tests/Fixtures/AssociatedEntityDto.php
@@ -0,0 +1,17 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
+
+class AssociatedEntityDto
+{
+ public $singleId;
+}
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
index 977f32e30fa61..05e5dae1b34ac 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Messenger/DoctrineTransactionMiddlewareTest.php
@@ -56,12 +56,9 @@ public function testMiddlewareWrapsInTransactionAndFlushes()
public function testTransactionIsRolledBackOnException()
{
- $this->connection->expects($this->once())
- ->method('beginTransaction')
- ;
- $this->connection->expects($this->once())
- ->method('rollBack')
- ;
+ $this->connection->expects($this->once())->method('beginTransaction');
+ $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true);
+ $this->connection->expects($this->once())->method('rollBack');
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Thrown from next middleware.');
@@ -69,6 +66,27 @@ public function testTransactionIsRolledBackOnException()
$this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock());
}
+ public function testExceptionInRollBackDoesNotHidePreviousException()
+ {
+ $this->connection->expects($this->once())->method('beginTransaction');
+ $this->connection->expects($this->once())->method('isTransactionActive')->willReturn(true);
+ $this->connection->expects($this->once())->method('rollBack')->willThrowException(new \RuntimeException('Thrown from rollBack.'));
+
+ try {
+ $this->middleware->handle(new Envelope(new \stdClass()), $this->getThrowingStackMock());
+ } catch (\Throwable $exception) {
+ }
+
+ self::assertNotNull($exception);
+ self::assertInstanceOf(\RuntimeException::class, $exception);
+ self::assertSame('Thrown from rollBack.', $exception->getMessage());
+
+ $previous = $exception->getPrevious();
+ self::assertNotNull($previous);
+ self::assertInstanceOf(\RuntimeException::class, $previous);
+ self::assertSame('Thrown from next middleware.', $previous->getMessage());
+ }
+
public function testInvalidEntityManagerThrowsException()
{
$managerRegistry = $this->createMock(ManagerRegistry::class);
diff --git a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php
index f554acb70d0fb..9f0341bdc7794 100644
--- a/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php
+++ b/src/Symfony/Bridge/Doctrine/Tests/Validator/Constraints/UniqueEntityValidatorTest.php
@@ -21,6 +21,7 @@
use Doctrine\Persistence\ObjectManager;
use PHPUnit\Framework\MockObject\MockObject;
use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper;
+use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociatedEntityDto;
use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\AssociationEntity2;
use Symfony\Bridge\Doctrine\Tests\Fixtures\CompositeIntIdEntity;
@@ -609,6 +610,40 @@ public function testAssociatedEntityWithNull()
$this->assertNoViolation();
}
+ public function testAssociatedEntityReferencedByPrimaryKey()
+ {
+ $this->registry = $this->createRegistryMock($this->em);
+ $this->registry->expects($this->any())
+ ->method('getManagerForClass')
+ ->willReturn($this->em);
+ $this->validator = $this->createValidator();
+ $this->validator->initialize($this->context);
+
+ $entity = new SingleIntIdEntity(1, 'foo');
+ $associated = new AssociationEntity();
+ $associated->single = $entity;
+
+ $this->em->persist($entity);
+ $this->em->persist($associated);
+ $this->em->flush();
+
+ $dto = new AssociatedEntityDto();
+ $dto->singleId = 1;
+
+ $this->validator->validate($dto, new UniqueEntity(
+ fields: ['singleId' => 'single'],
+ entityClass: AssociationEntity::class,
+ ));
+
+ $this->buildViolation('This value is already used.')
+ ->atPath('property.path.single')
+ ->setParameter('{{ value }}', 1)
+ ->setInvalidValue(1)
+ ->setCode(UniqueEntity::NOT_UNIQUE_ERROR)
+ ->setCause([$associated])
+ ->assertRaised();
+ }
+
public function testValidateUniquenessWithArrayValue()
{
$repository = $this->createRepositoryMock(SingleIntIdEntity::class);
diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
index 91ea6e5cdd359..365301829e28f 100644
--- a/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Extension/HttpKernelExtensionTest.php
@@ -80,7 +80,7 @@ public function testGenerateFragmentUri()
]);
$twig->addRuntimeLoader($loader);
- $this->assertSame('/_fragment?_hash=XCg0hX8QzSwik8Xuu9aMXhoCeI4oJOob7lUVacyOtyY%3D&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction', $twig->render('index'));
+ $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=template%3Dfoo.html.twig%26_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CController%255CTemplateController%253A%253AtemplateAction$#', $twig->render('index'));
}
protected function getFragmentHandler($returnOrException): FragmentHandler
diff --git a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php
index 96f707cdfdf2c..f6dd5f623baee 100644
--- a/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Extension/TranslationExtensionTest.php
@@ -206,6 +206,68 @@ public function testDefaultTranslationDomainWithNamedArguments()
$this->assertEquals('foo (custom)foo (foo)foo (custom)foo (custom)foo (fr)foo (custom)foo (fr)', trim($template->render([])));
}
+ public function testDefaultTranslationDomainWithExpression()
+ {
+ $templates = [
+ 'index' => '
+ {%- extends "base" %}
+
+ {%- trans_default_domain custom_domain %}
+
+ {%- block content %}
+ {{- "foo"|trans }}
+ {%- endblock %}
+ ',
+
+ 'base' => '
+ {%- block content "" %}
+ ',
+ ];
+
+ $translator = new Translator('en');
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', ['foo' => 'foo (messages)'], 'en');
+ $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom');
+ $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo');
+
+ $template = $this->getTemplate($templates, $translator);
+
+ $this->assertEquals('foo (foo)', trim($template->render(['custom_domain' => 'foo'])));
+ }
+
+ public function testDefaultTranslationDomainWithExpressionAndInheritance()
+ {
+ $templates = [
+ 'index' => '
+ {%- extends "base" %}
+
+ {%- trans_default_domain foo_domain %}
+
+ {%- block content %}
+ {{- "foo"|trans }}
+ {%- endblock %}
+ ',
+
+ 'base' => '
+ {%- trans_default_domain custom_domain %}
+
+ {{- "foo"|trans }}
+ {%- block content "" %}
+ {{- "foo"|trans }}
+ ',
+ ];
+
+ $translator = new Translator('en');
+ $translator->addLoader('array', new ArrayLoader());
+ $translator->addResource('array', ['foo' => 'foo (messages)'], 'en');
+ $translator->addResource('array', ['foo' => 'foo (custom)'], 'en', 'custom');
+ $translator->addResource('array', ['foo' => 'foo (foo)'], 'en', 'foo');
+
+ $template = $this->getTemplate($templates, $translator);
+
+ $this->assertEquals('foo (custom)foo (foo)foo (custom)', trim($template->render(['foo_domain' => 'foo', 'custom_domain' => 'custom'])));
+ }
+
private function getTemplate($template, ?TranslatorInterface $translator = null): TemplateWrapper
{
$translator ??= new Translator('en');
diff --git a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php
index 5c2bacf19d5f8..47ec58acb36cb 100644
--- a/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php
+++ b/src/Symfony/Bridge/Twig/Tests/Node/SearchAndRenderBlockNodeTest.php
@@ -22,6 +22,7 @@
use Twig\Node\Expression\ConditionalExpression;
use Twig\Node\Expression\ConstantExpression;
use Twig\Node\Expression\NameExpression;
+use Twig\Node\Expression\Ternary\ConditionalTernary;
use Twig\Node\Expression\Variable\ContextVariable;
use Twig\Node\Node;
use Twig\Node\Nodes;
@@ -308,32 +309,32 @@ public function testCompileLabelWithLabelAndAttributes()
public function testCompileLabelWithLabelThatEvaluatesToNull()
{
+ if (class_exists(ConditionalTernary::class)) {
+ $conditional = new ConditionalTernary(
+ // if
+ new ConstantExpression(true, 0),
+ // then
+ new ConstantExpression(null, 0),
+ // else
+ new ConstantExpression(null, 0),
+ 0
+ );
+ } else {
+ $conditional = new ConditionalExpression(
+ // if
+ new ConstantExpression(true, 0),
+ // then
+ new ConstantExpression(null, 0),
+ // else
+ new ConstantExpression(null, 0),
+ 0
+ );
+ }
+
if (class_exists(Nodes::class)) {
- $arguments = new Nodes([
- new ContextVariable('form', 0),
- new ConditionalExpression(
- // if
- new ConstantExpression(true, 0),
- // then
- new ConstantExpression(null, 0),
- // else
- new ConstantExpression(null, 0),
- 0
- ),
- ]);
+ $arguments = new Nodes([new ContextVariable('form', 0), $conditional]);
} else {
- $arguments = new Node([
- new NameExpression('form', 0),
- new ConditionalExpression(
- // if
- new ConstantExpression(true, 0),
- // then
- new ConstantExpression(null, 0),
- // else
- new ConstantExpression(null, 0),
- 0
- ),
- ]);
+ $arguments = new Node([new NameExpression('form', 0), $conditional]);
}
if (class_exists(FirstClassTwigCallableReady::class)) {
@@ -359,18 +360,32 @@ public function testCompileLabelWithLabelThatEvaluatesToNull()
public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes()
{
+ if (class_exists(ConditionalTernary::class)) {
+ $conditional = new ConditionalTernary(
+ // if
+ new ConstantExpression(true, 0),
+ // then
+ new ConstantExpression(null, 0),
+ // else
+ new ConstantExpression(null, 0),
+ 0
+ );
+ } else {
+ $conditional = new ConditionalExpression(
+ // if
+ new ConstantExpression(true, 0),
+ // then
+ new ConstantExpression(null, 0),
+ // else
+ new ConstantExpression(null, 0),
+ 0
+ );
+ }
+
if (class_exists(Nodes::class)) {
$arguments = new Nodes([
new ContextVariable('form', 0),
- new ConditionalExpression(
- // if
- new ConstantExpression(true, 0),
- // then
- new ConstantExpression(null, 0),
- // else
- new ConstantExpression(null, 0),
- 0
- ),
+ $conditional,
new ArrayExpression([
new ConstantExpression('foo', 0),
new ConstantExpression('bar', 0),
@@ -381,12 +396,7 @@ public function testCompileLabelWithLabelThatEvaluatesToNullAndAttributes()
} else {
$arguments = new Node([
new NameExpression('form', 0),
- new ConditionalExpression(
- new ConstantExpression(true, 0),
- new ConstantExpression(null, 0),
- new ConstantExpression(null, 0),
- 0
- ),
+ $conditional,
new ArrayExpression([
new ConstantExpression('foo', 0),
new ConstantExpression('bar', 0),
diff --git a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
index f0cafcb917b2b..a3e35c0cbe950 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Command/TranslationUpdateCommand.php
@@ -62,6 +62,10 @@ public function __construct(
private array $enabledLocales = [],
) {
parent::__construct();
+
+ if (!method_exists($writer, 'getFormats')) {
+ throw new \InvalidArgumentException(sprintf('The writer class "%s" does not implement the "getFormats()" method.', $writer::class));
+ }
}
protected function configure(): void
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php
new file mode 100644
index 0000000000000..7542191d0e83e
--- /dev/null
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/Compiler/TranslationUpdateCommandPass.php
@@ -0,0 +1,31 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler;
+
+use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
+use Symfony\Component\DependencyInjection\ContainerBuilder;
+
+class TranslationUpdateCommandPass implements CompilerPassInterface
+{
+ public function process(ContainerBuilder $container): void
+ {
+ if (!$container->hasDefinition('console.command.translation_extract')) {
+ return;
+ }
+
+ $translationWriterClass = $container->getParameterBag()->resolveValue($container->findDefinition('translation.writer')->getClass());
+
+ if (!method_exists($translationWriterClass, 'getFormats')) {
+ $container->removeDefinition('console.command.translation_extract');
+ }
+ }
+}
diff --git a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
index 1114246cca3eb..4b0f1e6674c13 100644
--- a/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
+++ b/src/Symfony/Bundle/FrameworkBundle/DependencyInjection/FrameworkExtension.php
@@ -120,6 +120,8 @@
use Symfony\Component\Mime\MimeTypeGuesserInterface;
use Symfony\Component\Mime\MimeTypes;
use Symfony\Component\Notifier\Bridge as NotifierBridge;
+use Symfony\Component\Notifier\Bridge\FakeChat\FakeChatTransportFactory;
+use Symfony\Component\Notifier\Bridge\FakeSms\FakeSmsTransportFactory;
use Symfony\Component\Notifier\ChatterInterface;
use Symfony\Component\Notifier\Notifier;
use Symfony\Component\Notifier\Recipient\Recipient;
@@ -2714,7 +2716,7 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
$container->removeDefinition('notifier.channel.email');
}
- foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms'] as $serviceId) {
+ foreach (['texter', 'chatter', 'notifier.channel.chat', 'notifier.channel.email', 'notifier.channel.sms', 'notifier.channel.push'] as $serviceId) {
if (!$container->hasDefinition($serviceId)) {
continue;
}
@@ -2762,8 +2764,6 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
NotifierBridge\Engagespot\EngagespotTransportFactory::class => 'notifier.transport_factory.engagespot',
NotifierBridge\Esendex\EsendexTransportFactory::class => 'notifier.transport_factory.esendex',
NotifierBridge\Expo\ExpoTransportFactory::class => 'notifier.transport_factory.expo',
- NotifierBridge\FakeChat\FakeChatTransportFactory::class => 'notifier.transport_factory.fake-chat',
- NotifierBridge\FakeSms\FakeSmsTransportFactory::class => 'notifier.transport_factory.fake-sms',
NotifierBridge\Firebase\FirebaseTransportFactory::class => 'notifier.transport_factory.firebase',
NotifierBridge\FortySixElks\FortySixElksTransportFactory::class => 'notifier.transport_factory.forty-six-elks',
NotifierBridge\FreeMobile\FreeMobileTransportFactory::class => 'notifier.transport_factory.free-mobile',
@@ -2847,20 +2847,26 @@ private function registerNotifierConfiguration(array $config, ContainerBuilder $
$container->removeDefinition($classToServices[NotifierBridge\Mercure\MercureTransportFactory::class]);
}
- if (ContainerBuilder::willBeAvailable('symfony/fake-chat-notifier', NotifierBridge\FakeChat\FakeChatTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) {
- $container->getDefinition($classToServices[NotifierBridge\FakeChat\FakeChatTransportFactory::class])
- ->replaceArgument(0, new Reference('mailer'))
- ->replaceArgument(1, new Reference('logger'))
+ // don't use ContainerBuilder::willBeAvailable() as these are not needed in production
+ if (class_exists(FakeChatTransportFactory::class)) {
+ $container->getDefinition('notifier.transport_factory.fake-chat')
+ ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
+ ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE));
+ } else {
+ $container->removeDefinition('notifier.transport_factory.fake-chat');
}
- if (ContainerBuilder::willBeAvailable('symfony/fake-sms-notifier', NotifierBridge\FakeSms\FakeSmsTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier', 'symfony/mailer'])) {
- $container->getDefinition($classToServices[NotifierBridge\FakeSms\FakeSmsTransportFactory::class])
- ->replaceArgument(0, new Reference('mailer'))
- ->replaceArgument(1, new Reference('logger'))
+ // don't use ContainerBuilder::willBeAvailable() as these are not needed in production
+ if (class_exists(FakeSmsTransportFactory::class)) {
+ $container->getDefinition('notifier.transport_factory.fake-sms')
+ ->replaceArgument(0, new Reference('mailer', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
+ ->replaceArgument(1, new Reference('logger', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('event_dispatcher', ContainerBuilder::NULL_ON_INVALID_REFERENCE))
->addArgument(new Reference('http_client', ContainerBuilder::NULL_ON_INVALID_REFERENCE));
+ } else {
+ $container->removeDefinition('notifier.transport_factory.fake-sms');
}
if (ContainerBuilder::willBeAvailable('symfony/bluesky-notifier', NotifierBridge\Bluesky\BlueskyTransportFactory::class, ['symfony/framework-bundle', 'symfony/notifier'])) {
diff --git a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
index a1eb059bb01ce..f26dbf54b49e0 100644
--- a/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
+++ b/src/Symfony/Bundle/FrameworkBundle/FrameworkBundle.php
@@ -19,6 +19,7 @@
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\RemoveUnusedSessionMarshallingHandlerPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerRealRefPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TestServiceContainerWeakRefPass;
+use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\TranslationUpdateCommandPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\Compiler\UnusedTagsPass;
use Symfony\Bundle\FrameworkBundle\DependencyInjection\VirtualRequestStackPass;
use Symfony\Component\Cache\Adapter\ApcuAdapter;
@@ -181,6 +182,7 @@ public function build(ContainerBuilder $container): void
// must be registered after MonologBundle's LoggerChannelPass
$container->addCompilerPass(new ErrorLoggerCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -32);
$container->addCompilerPass(new VirtualRequestStackPass());
+ $container->addCompilerPass(new TranslationUpdateCommandPass(), PassConfig::TYPE_BEFORE_REMOVING);
if ($container->getParameter('kernel.debug')) {
$container->addCompilerPass(new AddDebugLogProcessorPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 2);
diff --git a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php
index 3bd19b8ddc061..0bee3f3e86e2f 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Resources/config/notifier.php
@@ -73,7 +73,10 @@
->tag('notifier.channel', ['channel' => 'email'])
->set('notifier.channel.push', PushChannel::class)
- ->args([service('texter.transports'), service('messenger.default_bus')->ignoreOnInvalid()])
+ ->args([
+ service('texter.transports'),
+ abstract_arg('message bus'),
+ ])
->tag('notifier.channel', ['channel' => 'push'])
->set('notifier.monolog_handler', NotifierHandler::class)
diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
index 6d8966a171ba2..48d5c327a3986 100644
--- a/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
+++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Functional/FragmentTest.php
@@ -50,6 +50,6 @@ public function testGenerateFragmentUri()
$client = self::createClient(['test_case' => 'Fragment', 'root_config' => 'config.yml', 'debug' => true]);
$client->request('GET', '/fragment_uri');
- $this->assertSame('/_fragment?_hash=CCRGN2D%2FoAJbeGz%2F%2FdoH3bNSPwLCrmwC1zAYCGIKJ0E%3D&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction', $client->getResponse()->getContent());
+ $this->assertMatchesRegularExpression('#/_fragment\?_hash=.+&_path=_format%3Dhtml%26_locale%3Den%26_controller%3DSymfony%255CBundle%255CFrameworkBundle%255CTests%255CFunctional%255CBundle%255CTestBundle%255CController%255CFragmentController%253A%253AindexAction$#', $client->getResponse()->getContent());
}
}
diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
index af9f0a4ceaba3..55589c2945d88 100644
--- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
+++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Profiler/open.css.twig
@@ -40,6 +40,7 @@
#source .source-content ol li {
margin: 0 0 2px 0;
padding-left: 5px;
+ white-space: preserve nowrap;
}
#source .source-content ol li::marker {
color: var(--color-muted);
diff --git a/src/Symfony/Component/Console/Helper/ProgressBar.php b/src/Symfony/Component/Console/Helper/ProgressBar.php
index 7c22b7d55615f..619eafea23469 100644
--- a/src/Symfony/Component/Console/Helper/ProgressBar.php
+++ b/src/Symfony/Component/Console/Helper/ProgressBar.php
@@ -229,7 +229,7 @@ public function getEstimated(): float
public function getRemaining(): float
{
- if (!$this->step) {
+ if (0 === $this->step || $this->step === $this->startingStep) {
return 0;
}
diff --git a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
index 88b3e9228a2f2..3d1bfa48fce27 100644
--- a/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
+++ b/src/Symfony/Component/Console/Tests/Helper/ProgressBarTest.php
@@ -110,6 +110,16 @@ public function testRegularTimeEstimation()
);
}
+ public function testRegularTimeRemainingWithDifferentStartAtAndCustomDisplay()
+ {
+ $this->expectNotToPerformAssertions();
+
+ ProgressBar::setFormatDefinition('custom', ' %current%/%max% [%bar%] %percent:3s%% %remaining% %estimated%');
+ $bar = new ProgressBar($this->getOutputStream(), 1_200, 0);
+ $bar->setFormat('custom');
+ $bar->start(1_200, 600);
+ }
+
public function testResumedTimeEstimation()
{
$bar = new ProgressBar($output = $this->getOutputStream(), 1_200, 0);
diff --git a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt
index cfb10d03dafdd..81becafd8e350 100644
--- a/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt
+++ b/src/Symfony/Component/ErrorHandler/Tests/phpt/fatal_with_nested_handlers.phpt
@@ -24,7 +24,7 @@ var_dump([
$eHandler[0]->setExceptionHandler('print_r');
if (true) {
- class Broken implements \JsonSerializable
+ class Broken implements \Iterator
{
}
}
@@ -37,14 +37,14 @@ array(1) {
}
object(Symfony\Component\ErrorHandler\Error\FatalError)#%d (%d) {
["message":protected]=>
- string(186) "Error: Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)"
+ string(209) "Error: Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)"
%a
["error":"Symfony\Component\ErrorHandler\Error\FatalError":private]=>
array(4) {
["type"]=>
int(1)
["message"]=>
- string(179) "Class Symfony\Component\ErrorHandler\Broken contains 1 abstract method and must therefore be declared abstract or implement the remaining methods (JsonSerializable::jsonSerialize)"
+ string(202) "Class Symfony\Component\ErrorHandler\Broken contains 5 abstract methods and must therefore be declared abstract or implement the remaining methods (Iterator::current, Iterator::next, Iterator::key, ...)"
["file"]=>
string(%d) "%s"
["line"]=>
diff --git a/src/Symfony/Component/Finder/Comparator/DateComparator.php b/src/Symfony/Component/Finder/Comparator/DateComparator.php
index e0c523d05523b..f7c27de677fb1 100644
--- a/src/Symfony/Component/Finder/Comparator/DateComparator.php
+++ b/src/Symfony/Component/Finder/Comparator/DateComparator.php
@@ -36,7 +36,7 @@ public function __construct(string $test)
throw new \InvalidArgumentException(sprintf('"%s" is not a valid date.', $matches[2]));
}
- $operator = $matches[1] ?? '==';
+ $operator = $matches[1] ?: '==';
if ('since' === $operator || 'after' === $operator) {
$operator = '>';
}
diff --git a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
index 47bcc4838bd26..e50b713062638 100644
--- a/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
+++ b/src/Symfony/Component/Finder/Tests/Comparator/DateComparatorTest.php
@@ -59,6 +59,7 @@ public static function getTestData()
['after 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]],
['since 2005-10-10', [strtotime('2005-10-15')], [strtotime('2005-10-09')]],
['!= 2005-10-10', [strtotime('2005-10-11')], [strtotime('2005-10-10')]],
+ ['2005-10-10', [strtotime('2005-10-10')], [strtotime('2005-10-11')]],
];
}
}
diff --git a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
index fe0e0d39cd9d9..c00b8f7dfbfe5 100644
--- a/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
+++ b/src/Symfony/Component/HtmlSanitizer/Tests/TextSanitizer/UrlSanitizerTest.php
@@ -358,10 +358,10 @@ public static function provideParse(): iterable
'non-special://:@untrusted.com/x' => ['scheme' => 'non-special', 'host' => 'untrusted.com'],
'http:foo.com' => ['scheme' => 'http', 'host' => null],
" :foo.com \n" => null,
- ' foo.com ' => ['scheme' => null, 'host' => null],
+ ' foo.com ' => null,
'a: foo.com' => null,
- 'http://f:21/ b ? d # e ' => ['scheme' => 'http', 'host' => 'f'],
- 'lolscheme:x x#x x' => ['scheme' => 'lolscheme', 'host' => null],
+ 'http://f:21/ b ? d # e ' => null,
+ 'lolscheme:x x#x x' => null,
'http://f:/c' => ['scheme' => 'http', 'host' => 'f'],
'http://f:0/c' => ['scheme' => 'http', 'host' => 'f'],
'http://f:00000000000000/c' => ['scheme' => 'http', 'host' => 'f'],
@@ -434,7 +434,7 @@ public static function provideParse(): iterable
'javascript:example.com/' => ['scheme' => 'javascript', 'host' => null],
'mailto:example.com/' => ['scheme' => 'mailto', 'host' => null],
'/a/b/c' => ['scheme' => null, 'host' => null],
- '/a/ /c' => ['scheme' => null, 'host' => null],
+ '/a/ /c' => null,
'/a%2fc' => ['scheme' => null, 'host' => null],
'/a/%2f/c' => ['scheme' => null, 'host' => null],
'#β' => ['scheme' => null, 'host' => null],
@@ -495,10 +495,10 @@ public static function provideParse(): iterable
'http://example.com/你好你好' => ['scheme' => 'http', 'host' => 'example.com'],
'http://example.com/‥/foo' => ['scheme' => 'http', 'host' => 'example.com'],
"http://example.com/\u{feff}/foo" => ['scheme' => 'http', 'host' => 'example.com'],
- "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => ['scheme' => 'http', 'host' => 'example.com'],
+ "http://example.com\u{002f}\u{202e}\u{002f}\u{0066}\u{006f}\u{006f}\u{002f}\u{202d}\u{002f}\u{0062}\u{0061}\u{0072}\u{0027}\u{0020}" => null,
'http://www.google.com/foo?bar=baz#' => ['scheme' => 'http', 'host' => 'www.google.com'],
- 'http://www.google.com/foo?bar=baz# »' => ['scheme' => 'http', 'host' => 'www.google.com'],
- 'data:test# »' => ['scheme' => 'data', 'host' => null],
+ 'http://www.google.com/foo?bar=baz# »' => null,
+ 'data:test# »' => null,
'http://www.google.com' => ['scheme' => 'http', 'host' => 'www.google.com'],
'http://192.0x00A80001' => ['scheme' => 'http', 'host' => '192.0x00A80001'],
'http://www/foo%2Ehtml' => ['scheme' => 'http', 'host' => 'www'],
@@ -706,11 +706,11 @@ public static function provideParse(): iterable
'test-a-colon-slash-slash-b.html' => ['scheme' => null, 'host' => null],
'http://example.org/test?a#bc' => ['scheme' => 'http', 'host' => 'example.org'],
'http:\\/\\/f:b\\/c' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/f: \\/c' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/f: \\/c' => null,
'http:\\/\\/f:fifty-two\\/c' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/f:999999\\/c' => ['scheme' => 'http', 'host' => null],
'non-special:\\/\\/f:999999\\/c' => ['scheme' => 'non-special', 'host' => null],
- 'http:\\/\\/f: 21 \\/ b ? d # e ' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/f: 21 \\/ b ? d # e ' => null,
'http:\\/\\/[1::2]:3:4' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/2001::1' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/2001::1]' => ['scheme' => 'http', 'host' => null],
@@ -734,8 +734,8 @@ public static function provideParse(): iterable
'http:@:www.example.com' => ['scheme' => 'http', 'host' => null],
'http:\\/@:www.example.com' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/@:www.example.com' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/example example.com' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/Goo%20 goo%7C|.com' => ['scheme' => 'http', 'host' => null],
+ 'http:\\/\\/example example.com' => null,
+ 'http:\\/\\/Goo%20 goo%7C|.com' => null,
'http:\\/\\/[]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[:]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/GOO\\u00a0\\u3000goo.com' => ['scheme' => 'http', 'host' => null],
@@ -752,8 +752,8 @@ public static function provideParse(): iterable
'http:\\/\\/hello%00' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/192.168.0.257' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/%3g%78%63%30%2e%30%32%35%30%2E.01' => ['scheme' => 'http', 'host' => null],
- 'http:\\/\\/192.168.0.1 hello' => ['scheme' => 'http', 'host' => null],
- 'https:\\/\\/x x:12' => ['scheme' => 'https', 'host' => null],
+ 'http:\\/\\/192.168.0.1 hello' => null,
+ 'https:\\/\\/x x:12' => null,
'http:\\/\\/[www.google.com]\\/' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[google.com]' => ['scheme' => 'http', 'host' => null],
'http:\\/\\/[::1.2.3.4x]' => ['scheme' => 'http', 'host' => null],
@@ -763,7 +763,7 @@ public static function provideParse(): iterable
'..\\/i' => ['scheme' => null, 'host' => null],
'\\/i' => ['scheme' => null, 'host' => null],
'sc:\\/\\/\\u0000\\/' => ['scheme' => 'sc', 'host' => null],
- 'sc:\\/\\/ \\/' => ['scheme' => 'sc', 'host' => null],
+ 'sc:\\/\\/ \\/' => null,
'sc:\\/\\/@\\/' => ['scheme' => 'sc', 'host' => null],
'sc:\\/\\/te@s:t@\\/' => ['scheme' => 'sc', 'host' => null],
'sc:\\/\\/:\\/' => ['scheme' => 'sc', 'host' => null],
diff --git a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
index a806981de770f..05d86ba15da8e 100644
--- a/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
+++ b/src/Symfony/Component/HtmlSanitizer/TextSanitizer/UrlSanitizer.php
@@ -94,7 +94,13 @@ public static function parse(string $url): ?array
}
try {
- return UriString::parse($url);
+ $parsedUrl = UriString::parse($url);
+
+ if (preg_match('/\s/', $url)) {
+ return null;
+ }
+
+ return $parsedUrl;
} catch (SyntaxError) {
return null;
}
diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php
index 2248999f88e51..47efc39f97905 100644
--- a/src/Symfony/Component/HttpClient/CurlHttpClient.php
+++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php
@@ -197,13 +197,12 @@ public function request(string $method, string $url, array $options = []): Respo
$curlopts[\CURLOPT_RESOLVE] = $resolve;
}
+ $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
if ('POST' === $method) {
// Use CURLOPT_POST to have browser-like POST-to-GET redirects for 301, 302 and 303
$curlopts[\CURLOPT_POST] = true;
} elseif ('HEAD' === $method) {
$curlopts[\CURLOPT_NOBODY] = true;
- } else {
- $curlopts[\CURLOPT_CUSTOMREQUEST] = $method;
}
if ('\\' !== \DIRECTORY_SEPARATOR && $options['timeout'] < 1) {
diff --git a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
index 39c78a2e2795d..806531e73872f 100644
--- a/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
+++ b/src/Symfony/Component/HttpClient/NoPrivateNetworkHttpClient.php
@@ -20,7 +20,6 @@
use Symfony\Contracts\HttpClient\ChunkInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
-use Symfony\Contracts\HttpClient\ResponseStreamInterface;
use Symfony\Contracts\Service\ResetInterface;
/**
@@ -81,17 +80,6 @@ public function request(string $method, string $url, array $options = []): Respo
$ip = self::dnsResolve($dnsCache, $host, $this->ipFlags, $options);
self::ipCheck($ip, $this->subnets, $this->ipFlags, $host, $url);
- if (0 < $maxRedirects = $options['max_redirects']) {
- $options['max_redirects'] = 0;
- $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];
-
- if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
- $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
- return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
- });
- }
- }
-
$onProgress = $options['on_progress'] ?? null;
$subnets = $this->subnets;
$ipFlags = $this->ipFlags;
@@ -99,7 +87,7 @@ public function request(string $method, string $url, array $options = []): Respo
$options['on_progress'] = static function (int $dlNow, int $dlSize, array $info) use ($onProgress, $subnets, $ipFlags): void {
static $lastPrimaryIp = '';
- if (($info['primary_ip'] ?? '') !== $lastPrimaryIp) {
+ if (!\in_array($info['primary_ip'] ?? '', ['', $lastPrimaryIp], true)) {
self::ipCheck($info['primary_ip'], $subnets, $ipFlags, null, $info['url']);
$lastPrimaryIp = $info['primary_ip'];
}
@@ -107,6 +95,19 @@ public function request(string $method, string $url, array $options = []): Respo
null !== $onProgress && $onProgress($dlNow, $dlSize, $info);
};
+ if (0 >= $maxRedirects = $options['max_redirects']) {
+ return new AsyncResponse($this->client, $method, $url, $options);
+ }
+
+ $options['max_redirects'] = 0;
+ $redirectHeaders['with_auth'] = $redirectHeaders['no_auth'] = $options['headers'];
+
+ if (isset($options['normalized_headers']['host']) || isset($options['normalized_headers']['authorization']) || isset($options['normalized_headers']['cookie'])) {
+ $redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], static function ($h) {
+ return 0 !== stripos($h, 'Host:') && 0 !== stripos($h, 'Authorization:') && 0 !== stripos($h, 'Cookie:');
+ });
+ }
+
return new AsyncResponse($this->client, $method, $url, $options, static function (ChunkInterface $chunk, AsyncContext $context) use (&$method, &$options, $maxRedirects, &$redirectHeaders, $subnets, $ipFlags, $dnsCache): \Generator {
if (null !== $chunk->getError() || $chunk->isTimeout() || !$chunk->isFirst()) {
yield $chunk;
@@ -137,7 +138,7 @@ public function request(string $method, string $url, array $options = []): Respo
$filterContentHeaders = static function ($h) {
return 0 !== stripos($h, 'Content-Length:') && 0 !== stripos($h, 'Content-Type:') && 0 !== stripos($h, 'Transfer-Encoding:');
};
- $options['header'] = array_filter($options['header'], $filterContentHeaders);
+ $options['headers'] = array_filter($options['headers'], $filterContentHeaders);
$redirectHeaders['no_auth'] = array_filter($redirectHeaders['no_auth'], $filterContentHeaders);
$redirectHeaders['with_auth'] = array_filter($redirectHeaders['with_auth'], $filterContentHeaders);
}
@@ -158,11 +159,6 @@ public function request(string $method, string $url, array $options = []): Respo
});
}
- public function stream(ResponseInterface|iterable $responses, ?float $timeout = null): ResponseStreamInterface
- {
- return $this->client->stream($responses, $timeout);
- }
-
/**
* @deprecated since Symfony 7.1, configure the logger on the wrapped HTTP client directly instead
*/
diff --git a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php
index 91ec7ea5f2c7c..a7493100c431d 100644
--- a/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/DataCollector/HttpClientDataCollectorTest.php
@@ -24,11 +24,6 @@ public static function setUpBeforeClass(): void
TestHttpServer::start();
}
- public static function tearDownAfterClass(): void
- {
- TestHttpServer::stop();
- }
-
public function testItCollectsRequestCount()
{
$httpClient1 = $this->httpClientThatHasTracedRequests([
diff --git a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
index 8675cf1484a72..728f10d3e5787 100644
--- a/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
+++ b/src/Symfony/Component/HttpClient/Tests/HttpClientTestCase.php
@@ -523,6 +523,73 @@ public function testNoPrivateNetworkWithResolveAndRedirect()
$client->request('GET', 'http://localhost:8057/302?location=https://symfony.com/');
}
+ public function testNoPrivateNetwork304()
+ {
+ $client = $this->getHttpClient(__FUNCTION__);
+ $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32');
+ $response = $client->request('GET', 'http://localhost:8057/304', [
+ 'headers' => ['If-Match' => '"abc"'],
+ 'buffer' => false,
+ ]);
+
+ $this->assertSame(304, $response->getStatusCode());
+ $this->assertSame('', $response->getContent(false));
+ }
+
+ public function testNoPrivateNetwork302()
+ {
+ $client = $this->getHttpClient(__FUNCTION__);
+ $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32');
+ $response = $client->request('GET', 'http://localhost:8057/302/relative');
+
+ $body = $response->toArray();
+
+ $this->assertSame('/', $body['REQUEST_URI']);
+ $this->assertNull($response->getInfo('redirect_url'));
+
+ $response = $client->request('GET', 'http://localhost:8057/302/relative', [
+ 'max_redirects' => 0,
+ ]);
+
+ $this->assertSame(302, $response->getStatusCode());
+ $this->assertSame('http://localhost:8057/', $response->getInfo('redirect_url'));
+ }
+
+ public function testNoPrivateNetworkStream()
+ {
+ $client = $this->getHttpClient(__FUNCTION__);
+
+ $response = $client->request('GET', 'http://localhost:8057');
+ $client = new NoPrivateNetworkHttpClient($client, '104.26.14.6/32');
+
+ $response = $client->request('GET', 'http://localhost:8057');
+ $chunks = $client->stream($response);
+ $result = [];
+
+ foreach ($chunks as $r => $chunk) {
+ if ($chunk->isTimeout()) {
+ $result[] = 't';
+ } elseif ($chunk->isLast()) {
+ $result[] = 'l';
+ } elseif ($chunk->isFirst()) {
+ $result[] = 'f';
+ }
+ }
+
+ $this->assertSame($response, $r);
+ $this->assertSame(['f', 'l'], $result);
+
+ $chunk = null;
+ $i = 0;
+
+ foreach ($client->stream($response) as $chunk) {
+ ++$i;
+ }
+
+ $this->assertSame(1, $i);
+ $this->assertTrue($chunk->isLast());
+ }
+
public function testNoRedirectWithInvalidLocation()
{
$client = $this->getHttpClient(__FUNCTION__);
@@ -589,4 +656,48 @@ public function testDefaultContentType()
$this->assertSame(['abc' => 'def', 'content-type' => 'application/json', 'REQUEST_METHOD' => 'POST'], $response->toArray());
}
+
+ public function testHeadRequestWithClosureBody()
+ {
+ $p = TestHttpServer::start(8067);
+
+ try {
+ $client = $this->getHttpClient(__FUNCTION__);
+
+ $response = $client->request('HEAD', 'http://localhost:8057/head', [
+ 'body' => fn () => '',
+ ]);
+ $headers = $response->getHeaders();
+ } finally {
+ $p->stop();
+ }
+
+ $this->assertArrayHasKey('x-request-vars', $headers);
+
+ $vars = json_decode($headers['x-request-vars'][0], true);
+ $this->assertIsArray($vars);
+ $this->assertSame('HEAD', $vars['REQUEST_METHOD']);
+ }
+
+ /**
+ * @testWith [301]
+ * [302]
+ * [303]
+ */
+ public function testPostToGetRedirect(int $status)
+ {
+ $p = TestHttpServer::start(8067);
+
+ try {
+ $client = $this->getHttpClient(__FUNCTION__);
+
+ $response = $client->request('POST', 'http://localhost:8057/custom?status=' . $status . '&headers[]=Location%3A%20%2F');
+ $body = $response->toArray();
+ } finally {
+ $p->stop();
+ }
+
+ $this->assertSame('GET', $body['REQUEST_METHOD']);
+ $this->assertSame('/', $body['REQUEST_URI']);
+ }
}
diff --git a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
index f5f6d8ddcf9be..b500c9548ebb0 100644
--- a/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/HttplugClientTest.php
@@ -32,11 +32,6 @@ public static function setUpBeforeClass(): void
TestHttpServer::start();
}
- public static function tearDownAfterClass(): void
- {
- TestHttpServer::stop();
- }
-
/**
* @requires function ob_gzhandler
*/
diff --git a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
index fb940790b0b3f..06ffc128187cf 100644
--- a/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/NoPrivateNetworkHttpClientTest.php
@@ -173,6 +173,27 @@ public function testNonCallableOnProgressCallback()
$client->request('GET', $url, ['on_progress' => $customCallback]);
}
+ public function testHeadersArePassedOnRedirect()
+ {
+ $ipAddr = '104.26.14.6';
+ $url = sprintf('http://%s/', $ipAddr);
+ $content = 'foo';
+
+ $callback = function ($method, $url, $options) use ($content): MockResponse {
+ $this->assertArrayHasKey('headers', $options);
+ $this->assertNotContains('content-type: application/json', $options['headers']);
+ $this->assertContains('foo: bar', $options['headers']);
+ return new MockResponse($content);
+ };
+ $responses = [
+ new MockResponse('', ['http_code' => 302, 'redirect_url' => 'http://104.26.14.7']),
+ $callback,
+ ];
+ $client = new NoPrivateNetworkHttpClient(new MockHttpClient($responses));
+ $response = $client->request('POST', $url, ['headers' => ['foo' => 'bar', 'content-type' => 'application/json']]);
+ $this->assertEquals($content, $response->getContent());
+ }
+
private function getMockHttpClient(string $ipAddr, string $content)
{
return new MockHttpClient(new MockResponse($content, ['primary_ip' => $ipAddr]));
diff --git a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php
index 65b7f5b3f6794..bf49535ae3e66 100644
--- a/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/Psr18ClientTest.php
@@ -28,11 +28,6 @@ public static function setUpBeforeClass(): void
TestHttpServer::start();
}
- public static function tearDownAfterClass(): void
- {
- TestHttpServer::stop();
- }
-
/**
* @requires function ob_gzhandler
*/
diff --git a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php
index a0e39cc46c851..ba9504ae1c66d 100644
--- a/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php
@@ -27,11 +27,6 @@
class RetryableHttpClientTest extends TestCase
{
- public static function tearDownAfterClass(): void
- {
- TestHttpServer::stop();
- }
-
public function testRetryOnError()
{
$client = new RetryableHttpClient(
diff --git a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php
index b6a2c03c8f7a3..cf437a653bd76 100644
--- a/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php
+++ b/src/Symfony/Component/HttpClient/Tests/TraceableHttpClientTest.php
@@ -29,11 +29,6 @@ public static function setUpBeforeClass(): void
TestHttpServer::start();
}
- public static function tearDownAfterClass(): void
- {
- TestHttpServer::stop();
- }
-
public function testItTracesRequest()
{
$httpClient = $this->createMock(HttpClientInterface::class);
diff --git a/src/Symfony/Component/HttpClient/composer.json b/src/Symfony/Component/HttpClient/composer.json
index 1b3ae12804c51..250396d33ab1d 100644
--- a/src/Symfony/Component/HttpClient/composer.json
+++ b/src/Symfony/Component/HttpClient/composer.json
@@ -25,7 +25,7 @@
"php": ">=8.2",
"psr/log": "^1|^2|^3",
"symfony/deprecation-contracts": "^2.5|^3",
- "symfony/http-client-contracts": "~3.4.3|^3.5.1",
+ "symfony/http-client-contracts": "~3.4.4|^3.5.2",
"symfony/service-contracts": "^2.5|^3"
},
"require-dev": {
diff --git a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
index a2b160f8a2cc7..0f5b3fca55d45 100644
--- a/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
+++ b/src/Symfony/Component/HttpFoundation/BinaryFileResponse.php
@@ -189,7 +189,12 @@ public function prepare(Request $request): static
}
if (!$this->headers->has('Content-Type')) {
- $this->headers->set('Content-Type', $this->file->getMimeType() ?: 'application/octet-stream');
+ $mimeType = null;
+ if (!$this->tempFileObject) {
+ $mimeType = $this->file->getMimeType();
+ }
+
+ $this->headers->set('Content-Type', $mimeType ?: 'application/octet-stream');
}
parent::prepare($request);
diff --git a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
index 77bc32e8c5abc..d4d84b305b7fe 100644
--- a/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
+++ b/src/Symfony/Component/HttpFoundation/Tests/BinaryFileResponseTest.php
@@ -458,4 +458,15 @@ public function testCreateFromTemporaryFile()
$string = ob_get_clean();
$this->assertSame('foo,bar', $string);
}
+
+ public function testCreateFromTemporaryFileWithoutMimeType()
+ {
+ $file = new \SplTempFileObject();
+ $file->fwrite('foo,bar');
+
+ $response = new BinaryFileResponse($file);
+ $response->prepare(new Request());
+
+ $this->assertSame('application/octet-stream', $response->headers->get('Content-Type'));
+ }
}
diff --git a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
index c35d5e7e29381..5c94374fa2218 100644
--- a/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
+++ b/src/Symfony/Component/HttpKernel/Controller/ArgumentResolver/RequestPayloadValueResolver.php
@@ -46,11 +46,9 @@
class RequestPayloadValueResolver implements ValueResolverInterface, EventSubscriberInterface
{
/**
- * @see \Symfony\Component\Serializer\Normalizer\AbstractObjectNormalizer::DISABLE_TYPE_ENFORCEMENT
* @see DenormalizerInterface::COLLECT_DENORMALIZATION_ERRORS
*/
private const CONTEXT_DENORMALIZE = [
- 'disable_type_enforcement' => true,
'collect_denormalization_errors' => true,
];
@@ -189,7 +187,7 @@ private function mapQueryString(Request $request, ArgumentMetadata $argument, Ma
return null;
}
- return $this->serializer->denormalize($data, $argument->getType(), null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
+ return $this->serializer->denormalize($data, $argument->getType(), 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ['filter_bool' => true]);
}
private function mapRequestPayload(Request $request, ArgumentMetadata $argument, MapRequestPayload $attribute): object|array|null
@@ -209,7 +207,7 @@ private function mapRequestPayload(Request $request, ArgumentMetadata $argument,
}
if ($data = $request->request->all()) {
- return $this->serializer->denormalize($data, $type, null, $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
+ return $this->serializer->denormalize($data, $type, 'csv', $attribute->serializationContext + self::CONTEXT_DENORMALIZE + ('form' === $format ? ['filter_bool' => true] : []));
}
if ('' === $data = $request->getContent()) {
diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php
index dc038b0602468..cb0acb3448789 100644
--- a/src/Symfony/Component/HttpKernel/Kernel.php
+++ b/src/Symfony/Component/HttpKernel/Kernel.php
@@ -73,11 +73,11 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl
*/
private static array $freshCache = [];
- public const VERSION = '7.1.9';
- public const VERSION_ID = 70109;
+ public const VERSION = '7.1.10';
+ public const VERSION_ID = 70110;
public const MAJOR_VERSION = 7;
public const MINOR_VERSION = 1;
- public const RELEASE_VERSION = 9;
+ public const RELEASE_VERSION = 10;
public const EXTRA_VERSION = '';
public const END_OF_MAINTENANCE = '01/2025';
diff --git a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
index b277650b44b45..a3f0a8519241d 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Controller/ArgumentResolver/RequestPayloadValueResolverTest.php
@@ -22,6 +22,7 @@
use Symfony\Component\HttpKernel\Exception\HttpException;
use Symfony\Component\HttpKernel\Exception\NearMissValueResolverException;
use Symfony\Component\HttpKernel\HttpKernelInterface;
+use Symfony\Component\PropertyInfo\Extractor\ReflectionExtractor;
use Symfony\Component\Serializer\Encoder\JsonEncoder;
use Symfony\Component\Serializer\Encoder\XmlEncoder;
use Symfony\Component\Serializer\Exception\NotNormalizableValueException;
@@ -395,6 +396,38 @@ public function testQueryStringValidationPassed()
$this->assertEquals([$payload], $event->getArguments());
}
+ public function testQueryStringParameterTypeMismatch()
+ {
+ $query = ['price' => 'not a float'];
+
+ $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
+ $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
+
+ $validator = $this->createMock(ValidatorInterface::class);
+ $validator->expects($this->never())->method('validate');
+
+ $resolver = new RequestPayloadValueResolver($serializer, $validator);
+
+ $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
+ MapQueryString::class => new MapQueryString(),
+ ]);
+
+ $request = Request::create('/', 'GET', $query);
+
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $arguments = $resolver->resolve($request, $argument);
+ $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
+
+ try {
+ $resolver->onKernelControllerArguments($event);
+ $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
+ } catch (HttpException $e) {
+ $validationFailedException = $e->getPrevious();
+ $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
+ $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
+ }
+ }
+
public function testRequestInputValidationPassed()
{
$input = ['price' => '50'];
@@ -457,6 +490,38 @@ public function testRequestArrayDenormalization()
$this->assertEquals([$payload], $event->getArguments());
}
+ public function testRequestInputTypeMismatch()
+ {
+ $input = ['price' => 'not a float'];
+
+ $normalizer = new ObjectNormalizer(null, null, null, new ReflectionExtractor());
+ $serializer = new Serializer([$normalizer], ['json' => new JsonEncoder()]);
+
+ $validator = $this->createMock(ValidatorInterface::class);
+ $validator->expects($this->never())->method('validate');
+
+ $resolver = new RequestPayloadValueResolver($serializer, $validator);
+
+ $argument = new ArgumentMetadata('invalid', RequestPayload::class, false, false, null, false, [
+ MapRequestPayload::class => new MapRequestPayload(),
+ ]);
+
+ $request = Request::create('/', 'POST', $input);
+
+ $kernel = $this->createMock(HttpKernelInterface::class);
+ $arguments = $resolver->resolve($request, $argument);
+ $event = new ControllerArgumentsEvent($kernel, function () {}, $arguments, $request, HttpKernelInterface::MAIN_REQUEST);
+
+ try {
+ $resolver->onKernelControllerArguments($event);
+ $this->fail(sprintf('Expected "%s" to be thrown.', HttpException::class));
+ } catch (HttpException $e) {
+ $validationFailedException = $e->getPrevious();
+ $this->assertInstanceOf(ValidationFailedException::class, $validationFailedException);
+ $this->assertSame('This value should be of type float.', $validationFailedException->getViolations()[0]->getMessage());
+ }
+ }
+
public function testItThrowsOnMissingAttributeType()
{
$serializer = new Serializer();
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
index fa9885d2753cd..43c740ee12b98 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/EsiFragmentRendererTest.php
@@ -60,8 +60,8 @@ public function testRenderControllerReference()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertEquals(
- '',
+ $this->assertMatchesRegularExpression(
+ '#^$#',
$strategy->render($reference, $request, ['alt' => $altReference])->getContent()
);
}
@@ -78,8 +78,8 @@ public function testRenderControllerReferenceWithAbsoluteUri()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertSame(
- '',
+ $this->assertMatchesRegularExpression(
+ '#^$#',
$strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent()
);
}
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
index f74887ade36f4..8e4b59e5feeb9 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/HIncludeFragmentRendererTest.php
@@ -32,7 +32,7 @@ public function testRenderWithControllerAndSigner()
{
$strategy = new HIncludeFragmentRenderer(null, new UriSigner('foo'));
- $this->assertEquals('', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent());
+ $this->assertMatchesRegularExpression('#^$#', $strategy->render(new ControllerReference('main_controller', [], []), Request::create('/'))->getContent());
}
public function testRenderWithUri()
diff --git a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
index 4af00f9f75137..7fd04c5a5b0b7 100644
--- a/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
+++ b/src/Symfony/Component/HttpKernel/Tests/Fragment/SsiFragmentRendererTest.php
@@ -51,8 +51,8 @@ public function testRenderControllerReference()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertEquals(
- '',
+ $this->assertMatchesRegularExpression(
+ '{^$}',
$strategy->render($reference, $request, ['alt' => $altReference])->getContent()
);
}
@@ -69,8 +69,8 @@ public function testRenderControllerReferenceWithAbsoluteUri()
$reference = new ControllerReference('main_controller', [], []);
$altReference = new ControllerReference('alt_controller', [], []);
- $this->assertSame(
- '',
+ $this->assertMatchesRegularExpression(
+ '{^$}',
$strategy->render($reference, $request, ['alt' => $altReference, 'absolute_uri' => true])->getContent()
);
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
index f4cc745846584..bf54ac1272483 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Tests/Transport/ConnectionTest.php
@@ -330,4 +330,25 @@ public function testSendWhenABeanstalkdExceptionOccurs()
$connection->send($body, $headers, $delay);
}
+
+ public function testSendWithRoundedDelay()
+ {
+ $tube = 'xyz';
+ $body = 'foo';
+ $headers = ['test' => 'bar'];
+ $delay = 920;
+ $expectedDelay = 0;
+
+ $client = $this->createMock(PheanstalkInterface::class);
+ $client->expects($this->once())->method('useTube')->with($tube)->willReturn($client);
+ $client->expects($this->once())->method('put')->with(
+ $this->anything(),
+ $this->anything(),
+ $expectedDelay,
+ $this->anything(),
+ );
+
+ $connection = new Connection(['tube_name' => $tube], $client);
+ $connection->send($body, $headers, $delay);
+ }
}
diff --git a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
index c85022d423e36..8821ee5e2b35a 100644
--- a/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
+++ b/src/Symfony/Component/Messenger/Bridge/Beanstalkd/Transport/Connection.php
@@ -124,7 +124,7 @@ public function send(string $body, array $headers, int $delay = 0): string
$job = $this->client->useTube($this->tube)->put(
$message,
PheanstalkInterface::DEFAULT_PRIORITY,
- $delay / 1000,
+ (int) ($delay / 1000),
$this->ttr
);
} catch (Exception $exception) {
diff --git a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
index 5e99e9d82579a..4ff2d53fc9b2a 100644
--- a/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
+++ b/src/Symfony/Component/Messenger/Bridge/Doctrine/Tests/Transport/ConnectionTest.php
@@ -14,7 +14,6 @@
use Doctrine\DBAL\Connection as DBALConnection;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\Platforms\AbstractPlatform;
-use Doctrine\DBAL\Platforms\MariaDb1060Platform;
use Doctrine\DBAL\Platforms\MariaDBPlatform;
use Doctrine\DBAL\Platforms\MySQL57Platform;
use Doctrine\DBAL\Platforms\MySQL80Platform;
@@ -590,9 +589,16 @@ class_exists(MySQLPlatform::class) ? new MySQLPlatform() : new MySQL57Platform()
'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE',
];
- if (class_exists(MariaDb1060Platform::class)) {
+ if (interface_exists(DBALException::class)) {
+ // DBAL 4+
+ $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDB1060Platform';
+ } else {
+ $mariaDbPlatformClass = 'Doctrine\DBAL\Platforms\MariaDb1060Platform';
+ }
+
+ if (class_exists($mariaDbPlatformClass)) {
yield 'MariaDB106' => [
- new MariaDb1060Platform(),
+ new $mariaDbPlatformClass(),
'SELECT m.* FROM messenger_messages m WHERE (m.queue_name = ?) AND (m.delivered_at is null OR m.delivered_at < ?) AND (m.available_at <= ?) ORDER BY available_at ASC LIMIT 1 FOR UPDATE SKIP LOCKED',
];
}
diff --git a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
index ed9c6e165aad6..4a9570d5b6982 100644
--- a/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
+++ b/src/Symfony/Component/Mime/Part/Multipart/FormDataPart.php
@@ -24,7 +24,7 @@
final class FormDataPart extends AbstractMultipartPart
{
/**
- * @param array $fields
+ * @param array $fields
*/
public function __construct(
private array $fields = [],
diff --git a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php
index e60d1f3160543..e6a5e099a0cde 100644
--- a/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php
+++ b/src/Symfony/Component/PropertyInfo/Extractor/ReflectionExtractor.php
@@ -705,8 +705,12 @@ private function isAllowedProperty(string $class, string $property, bool $writeA
return false;
}
- if (\PHP_VERSION_ID >= 80400 && ($reflectionProperty->isProtectedSet() || $reflectionProperty->isPrivateSet())) {
- return false;
+ if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isProtectedSet()) {
+ return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PROTECTED);
+ }
+
+ if (\PHP_VERSION_ID >= 80400 && $reflectionProperty->isPrivateSet()) {
+ return (bool) ($this->propertyReflectionFlags & \ReflectionProperty::IS_PRIVATE);
}
if (\PHP_VERSION_ID >= 80400 &&$reflectionProperty->isVirtual() && !$reflectionProperty->hasHook(\PropertyHookType::Set)) {
diff --git a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
index 8de070dc046c9..d006e32483896 100644
--- a/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
+++ b/src/Symfony/Component/PropertyInfo/PropertyReadInfo.php
@@ -15,8 +15,6 @@
* The property read info tells how a property can be read.
*
* @author Joel Wurtz
- *
- * @internal
*/
final class PropertyReadInfo
{
diff --git a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
index 6bc7abcdf849e..81ce7eda6d5b0 100644
--- a/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
+++ b/src/Symfony/Component/PropertyInfo/PropertyWriteInfo.php
@@ -15,8 +15,6 @@
* The write mutator defines how a property can be written.
*
* @author Joel Wurtz
- *
- * @internal
*/
final class PropertyWriteInfo
{
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
index 6248e4966dc15..0d77497c2e1da 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/PhpStanExtractorTest.php
@@ -14,15 +14,18 @@
use PHPUnit\Framework\TestCase;
use Symfony\Component\PropertyInfo\Extractor\PhpDocExtractor;
use Symfony\Component\PropertyInfo\Extractor\PhpStanExtractor;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\Clazz;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ConstructorDummyWithoutDocBlock;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DefaultValue;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DockBlockFallback;
use Symfony\Component\PropertyInfo\Tests\Fixtures\Dummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyCollection;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyGeneric;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyNamespace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyPropertyAndGetterWithDifferentTypes;
use Symfony\Component\PropertyInfo\Tests\Fixtures\DummyUnionType;
+use Symfony\Component\PropertyInfo\Tests\Fixtures\IFace;
use Symfony\Component\PropertyInfo\Tests\Fixtures\IntRangeDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\InvalidDummy;
use Symfony\Component\PropertyInfo\Tests\Fixtures\ParentDummy;
@@ -552,6 +555,77 @@ public static function allowPrivateAccessLegacyProvider(): array
];
}
+ /**
+ * @param list $expectedTypes
+ *
+ * @dataProvider legacyGenericsProvider
+ */
+ public function testGenericsLegacy(string $property, array $expectedTypes)
+ {
+ $this->assertEquals($expectedTypes, $this->extractor->getTypes(DummyGeneric::class, $property));
+ }
+
+ /**
+ * @return iterable}>
+ */
+ public static function legacyGenericsProvider(): iterable
+ {
+ yield [
+ 'basicClass',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Clazz::class,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'nullableClass',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Clazz::class,
+ nullable: true,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'basicInterface',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: IFace::class,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ yield [
+ 'nullableInterface',
+ [
+ new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: IFace::class,
+ nullable: true,
+ collectionValueType: new LegacyType(
+ builtinType: LegacyType::BUILTIN_TYPE_OBJECT,
+ class: Dummy::class,
+ )
+ ),
+ ],
+ ];
+ }
+
/**
* @dataProvider typesProvider
*/
@@ -968,7 +1042,41 @@ public static function allowPrivateAccessProvider(): array
public function testGenericInterface()
{
- $this->assertNull($this->extractor->getTypes(Dummy::class, 'genericInterface'));
+ $this->assertEquals(
+ Type::generic(Type::enum(\BackedEnum::class), Type::string()),
+ $this->extractor->getType(Dummy::class, 'genericInterface'),
+ );
+ }
+
+ /**
+ * @dataProvider genericsProvider
+ */
+ public function testGenerics(string $property, Type $expectedType)
+ {
+ $this->assertEquals($expectedType, $this->extractor->getType(DummyGeneric::class, $property));
+ }
+
+ /**
+ * @return iterable
+ */
+ public static function genericsProvider(): iterable
+ {
+ yield [
+ 'basicClass',
+ Type::generic(Type::object(Clazz::class), Type::object(Dummy::class)),
+ ];
+ yield [
+ 'nullableClass',
+ Type::nullable(Type::generic(Type::object(Clazz::class), Type::object(Dummy::class))),
+ ];
+ yield [
+ 'basicInterface',
+ Type::generic(Type::object(IFace::class), Type::object(Dummy::class)),
+ ];
+ yield [
+ 'nullableInterface',
+ Type::nullable(Type::generic(Type::object(IFace::class), Type::object(Dummy::class))),
+ ];
}
}
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
index 5ef776be26bd2..6e29bdaef1904 100644
--- a/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
+++ b/src/Symfony/Component/PropertyInfo/Tests/Extractor/ReflectionExtractorTest.php
@@ -704,6 +704,51 @@ public function testAsymmetricVisibility()
$this->assertFalse($this->extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
}
+ /**
+ * @requires PHP 8.4
+ */
+ public function testAsymmetricVisibilityAllowPublicOnly()
+ {
+ $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PUBLIC);
+
+ $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
+ }
+
+ /**
+ * @requires PHP 8.4
+ */
+ public function testAsymmetricVisibilityAllowProtectedOnly()
+ {
+ $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PROTECTED);
+
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertTrue($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
+ }
+
+ /**
+ * @requires PHP 8.4
+ */
+ public function testAsymmetricVisibilityAllowPrivateOnly()
+ {
+ $extractor = new ReflectionExtractor(null, null, null, true, ReflectionExtractor::ALLOW_PRIVATE);
+
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertFalse($extractor->isReadable(AsymmetricVisibility::class, 'protectedPrivate'));
+ $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'publicPrivate'));
+ $this->assertFalse($extractor->isWritable(AsymmetricVisibility::class, 'publicProtected'));
+ $this->assertTrue($extractor->isWritable(AsymmetricVisibility::class, 'protectedPrivate'));
+ }
+
/**
* @requires PHP 8.4
*/
diff --git a/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php
new file mode 100644
index 0000000000000..5863fbfc95450
--- /dev/null
+++ b/src/Symfony/Component/PropertyInfo/Tests/Fixtures/DummyGeneric.php
@@ -0,0 +1,41 @@
+
+ *
+ * For the full copyright and license information, please view the LICENSE
+ * file that was distributed with this source code.
+ */
+
+namespace Symfony\Component\PropertyInfo\Tests\Fixtures;
+
+interface IFace {}
+
+class Clazz {}
+
+class DummyGeneric
+{
+
+ /**
+ * @var Clazz
+ */
+ public $basicClass;
+
+ /**
+ * @var ?Clazz
+ */
+ public $nullableClass;
+
+ /**
+ * @var IFace
+ */
+ public $basicInterface;
+
+ /**
+ * @var ?IFace
+ */
+ public $nullableInterface;
+
+}
diff --git a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
index 6d06d8b8a9f16..924d74e3aec1a 100644
--- a/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
+++ b/src/Symfony/Component/PropertyInfo/Util/PhpStanTypeHelper.php
@@ -128,7 +128,7 @@ private function extractTypes(TypeNode $node, NameScope $nameScope): array
$collection = $mainType->isCollection() || \is_a($mainType->getClassName(), \Traversable::class, true) || \is_a($mainType->getClassName(), \ArrayAccess::class, true);
// it's safer to fall back to other extractors if the generic type is too abstract
- if (!$collection && !class_exists($mainType->getClassName())) {
+ if (!$collection && !class_exists($mainType->getClassName()) && !interface_exists($mainType->getClassName(), false)) {
return [];
}
diff --git a/src/Symfony/Component/PropertyInfo/composer.json b/src/Symfony/Component/PropertyInfo/composer.json
index 70a57ef06ced0..c033ca51befdc 100644
--- a/src/Symfony/Component/PropertyInfo/composer.json
+++ b/src/Symfony/Component/PropertyInfo/composer.json
@@ -25,7 +25,7 @@
"require": {
"php": ">=8.2",
"symfony/string": "^6.4|^7.0",
- "symfony/type-info": "^7.1"
+ "symfony/type-info": "~7.1.9|^7.2.2"
},
"require-dev": {
"symfony/serializer": "^6.4|^7.0",
diff --git a/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php b/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php
index d668b7d03b02b..1954d4f470402 100644
--- a/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php
+++ b/src/Symfony/Component/Scheduler/Tests/RecurringMessageTest.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\Scheduler\Tests;
use PHPUnit\Framework\TestCase;
-use Random\Randomizer;
use Symfony\Component\Scheduler\Exception\InvalidArgumentException;
use Symfony\Component\Scheduler\RecurringMessage;
@@ -22,13 +21,8 @@ public function testCanCreateHashedCronMessage()
{
$object = new DummyStringableMessage();
- if (class_exists(Randomizer::class)) {
- $this->assertSame('30 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger());
- $this->assertSame('30 0 * * 3', (string) RecurringMessage::cron('#weekly', $object)->getTrigger());
- } else {
- $this->assertSame('36 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger());
- $this->assertSame('36 0 * * 6', (string) RecurringMessage::cron('#weekly', $object)->getTrigger());
- }
+ $this->assertSame('30 0 * * *', (string) RecurringMessage::cron('#midnight', $object)->getTrigger());
+ $this->assertSame('30 0 * * 3', (string) RecurringMessage::cron('#weekly', $object)->getTrigger());
}
public function testHashedCronContextIsRequiredIfMessageIsNotStringable()
diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php
index cf12a7ceccf52..a700372d4765c 100644
--- a/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php
+++ b/src/Symfony/Component/Scheduler/Tests/Trigger/CronExpressionTriggerTest.php
@@ -12,7 +12,6 @@
namespace Symfony\Component\Scheduler\Tests\Trigger;
use PHPUnit\Framework\TestCase;
-use Random\Randomizer;
use Symfony\Component\Scheduler\Trigger\CronExpressionTrigger;
class CronExpressionTriggerTest extends TestCase
@@ -33,54 +32,28 @@ public function testHashedExpressionParsing(string $input, string $expected)
public static function hashedExpressionProvider(): array
{
- if (class_exists(Randomizer::class)) {
- return [
- ['# * * * *', '30 * * * *'],
- ['# # * * *', '30 0 * * *'],
- ['# # # * *', '30 0 25 * *'],
- ['# # # # *', '30 0 25 10 *'],
- ['# # # # #', '30 0 25 10 5'],
- ['# # 1,15 1-11 *', '30 0 1,15 1-11 *'],
- ['# # 1,15 * *', '30 0 1,15 * *'],
- ['#hourly', '30 * * * *'],
- ['#daily', '30 0 * * *'],
- ['#weekly', '30 0 * * 3'],
- ['#weekly@midnight', '30 0 * * 3'],
- ['#monthly', '30 0 25 * *'],
- ['#monthly@midnight', '30 0 25 * *'],
- ['#yearly', '30 0 25 10 *'],
- ['#yearly@midnight', '30 0 25 10 *'],
- ['#annually', '30 0 25 10 *'],
- ['#annually@midnight', '30 0 25 10 *'],
- ['#midnight', '30 0 * * *'],
- ['#(1-15) * * * *', '1 * * * *'],
- ['#(1-15) * * * #(3-5)', '1 * * * 3'],
- ['#(1-15) * # * #(3-5)', '1 * 17 * 5'],
- ];
- }
-
return [
- ['# * * * *', '36 * * * *'],
- ['# # * * *', '36 0 * * *'],
- ['# # # * *', '36 0 14 * *'],
- ['# # # # *', '36 0 14 3 *'],
- ['# # # # #', '36 0 14 3 5'],
- ['# # 1,15 1-11 *', '36 0 1,15 1-11 *'],
- ['# # 1,15 * *', '36 0 1,15 * *'],
- ['#hourly', '36 * * * *'],
- ['#daily', '36 0 * * *'],
- ['#weekly', '36 0 * * 6'],
- ['#weekly@midnight', '36 0 * * 6'],
- ['#monthly', '36 0 14 * *'],
- ['#monthly@midnight', '36 0 14 * *'],
- ['#yearly', '36 0 14 3 *'],
- ['#yearly@midnight', '36 0 14 3 *'],
- ['#annually', '36 0 14 3 *'],
- ['#annually@midnight', '36 0 14 3 *'],
- ['#midnight', '36 0 * * *'],
- ['#(1-15) * * * *', '7 * * * *'],
- ['#(1-15) * * * #(3-5)', '7 * * * 3'],
- ['#(1-15) * # * #(3-5)', '7 * 1 * 5'],
+ ['# * * * *', '30 * * * *'],
+ ['# # * * *', '30 0 * * *'],
+ ['# # # * *', '30 0 25 * *'],
+ ['# # # # *', '30 0 25 10 *'],
+ ['# # # # #', '30 0 25 10 5'],
+ ['# # 1,15 1-11 *', '30 0 1,15 1-11 *'],
+ ['# # 1,15 * *', '30 0 1,15 * *'],
+ ['#hourly', '30 * * * *'],
+ ['#daily', '30 0 * * *'],
+ ['#weekly', '30 0 * * 3'],
+ ['#weekly@midnight', '30 0 * * 3'],
+ ['#monthly', '30 0 25 * *'],
+ ['#monthly@midnight', '30 0 25 * *'],
+ ['#yearly', '30 0 25 10 *'],
+ ['#yearly@midnight', '30 0 25 10 *'],
+ ['#annually', '30 0 25 10 *'],
+ ['#annually@midnight', '30 0 25 10 *'],
+ ['#midnight', '30 0 * * *'],
+ ['#(1-15) * * * *', '1 * * * *'],
+ ['#(1-15) * * * #(3-5)', '1 * * * 3'],
+ ['#(1-15) * # * #(3-5)', '1 * 17 * 5'],
];
}
diff --git a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
index dc10d30307484..f8724e59247c1 100644
--- a/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
+++ b/src/Symfony/Component/Scheduler/Tests/Trigger/PeriodicalTriggerTest.php
@@ -105,9 +105,9 @@ public static function provideForToString()
/**
* @dataProvider providerGetNextRunDates
*/
- public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count = 0)
+ public function testGetNextRunDates(\DateTimeImmutable $from, TriggerInterface $trigger, array $expected, int $count)
{
- $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count ?? \count($expected)));
+ $this->assertEquals($expected, $this->getNextRunDates($from, $trigger, $count));
}
public static function providerGetNextRunDates(): iterable
diff --git a/src/Symfony/Component/Security/Http/CHANGELOG.md b/src/Symfony/Component/Security/Http/CHANGELOG.md
index a8e710597a989..de3dec5120bb3 100644
--- a/src/Symfony/Component/Security/Http/CHANGELOG.md
+++ b/src/Symfony/Component/Security/Http/CHANGELOG.md
@@ -6,7 +6,7 @@ CHANGELOG
* Add `#[IsCsrfTokenValid]` attribute
* Add CAS 2.0 access token handler
- * Make empty username or empty password on form login attempts return Bad Request (400)
+ * Make empty username or empty password on form login attempts throw `BadCredentialsException`
7.0
---
diff --git a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
index 22812267b6466..c8b22e7d717a0 100644
--- a/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
+++ b/src/Symfony/Component/TypeInfo/Tests/TypeResolver/StringTypeResolverTest.php
@@ -16,7 +16,9 @@
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyBackedEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyCollection;
+use Symfony\Component\TypeInfo\Tests\Fixtures\DummyEnum;
use Symfony\Component\TypeInfo\Tests\Fixtures\DummyWithTemplates;
use Symfony\Component\TypeInfo\Type;
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
@@ -138,6 +140,8 @@ public static function resolveDataProvider(): iterable
yield [Type::object(Dummy::class), 'static', $typeContextFactory->createFromClassName(Dummy::class, AbstractDummy::class)];
yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->createFromClassName(Dummy::class)];
yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->createFromClassName(Dummy::class)];
+ yield [Type::enum(DummyEnum::class), 'DummyEnum', $typeContextFactory->createFromClassName(DummyEnum::class)];
+ yield [Type::enum(DummyBackedEnum::class), 'DummyBackedEnum', $typeContextFactory->createFromClassName(DummyBackedEnum::class)];
yield [Type::template('T', Type::union(Type::int(), Type::string())), 'T', $typeContextFactory->createFromClassName(DummyWithTemplates::class)];
yield [Type::template('V'), 'V', $typeContextFactory->createFromReflection(new \ReflectionMethod(DummyWithTemplates::class, 'getPrice'))];
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
index 6af8feb370ef2..39672559365b2 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/ReflectionTypeResolver.php
@@ -27,11 +27,6 @@
*/
final class ReflectionTypeResolver implements TypeResolverInterface
{
- /**
- * @var array
- */
- private static array $reflectionEnumCache = [];
-
public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
{
if ($subject instanceof \ReflectionUnionType) {
@@ -83,11 +78,7 @@ public function resolve(mixed $subject, ?TypeContext $typeContext = null): Type
default => $identifier,
};
- if (is_subclass_of($className, \BackedEnum::class)) {
- $reflectionEnum = (self::$reflectionEnumCache[$className] ??= new \ReflectionEnum($className));
- $backingType = $this->resolve($reflectionEnum->getBackingType(), $typeContext);
- $type = Type::enum($className, $backingType);
- } elseif (is_subclass_of($className, \UnitEnum::class)) {
+ if (is_subclass_of($className, \UnitEnum::class)) {
$type = Type::enum($className);
} else {
$type = Type::object($className);
diff --git a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
index a8d8c600cdbee..a20946424f15d 100644
--- a/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
+++ b/src/Symfony/Component/TypeInfo/TypeResolver/StringTypeResolver.php
@@ -243,14 +243,16 @@ private function resolveCustomIdentifier(string $identifier, ?TypeContext $typeC
try {
new \ReflectionClass($className);
self::$classExistCache[$className] = true;
-
- return Type::object($className);
} catch (\Throwable) {
}
}
}
if (self::$classExistCache[$className]) {
+ if (is_subclass_of($className, \UnitEnum::class)) {
+ return Type::enum($className);
+ }
+
return Type::object($className);
}
diff --git a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
index 3977f37433060..485d69add1ee8 100644
--- a/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
+++ b/src/Symfony/Component/Validator/Resources/translations/validators.fa.xlf
@@ -444,27 +444,27 @@
This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.
- This value is too short. It should contain at least one word.|This value is too short. It should contain at least {{ min }} words.
+ این مقدار بسیار کوتاه است. باید حداقل یک کلمه داشته باشد.|این مقدار بسیار کوتاه است. باید حداقل {{ min }} کلمه داشته باشد.
This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.
- This value is too long. It should contain one word.|This value is too long. It should contain {{ max }} words or less.
+ این مقدار بیش از حد طولانی است. باید فقط یک کلمه باشد.|این مقدار بیش از حد طولانی است. باید حداکثر {{ max }} کلمه داشته باشد.
This value does not represent a valid week in the ISO 8601 format.
- This value does not represent a valid week in the ISO 8601 format.
+ این مقدار یک هفته معتبر در قالب ISO 8601 را نشان نمیدهد.
This value is not a valid week.
- This value is not a valid week.
+ این مقدار یک هفته معتبر نیست.
This value should not be before week "{{ min }}".
- This value should not be before week "{{ min }}".
+ این مقدار نباید قبل از هفته "{{ min }}" باشد.
This value should not be after week "{{ max }}".
- This value should not be after week "{{ max }}".
+ این مقدار نباید بعد از هفته "{{ max }}" باشد.