diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index b6f39741d9dbc..ac82d7063d015 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,6 +1,6 @@ | Q | A | ------------- | --- -| Branch? | master for features / 2.8 up to 4.1 for bug fixes +| Branch? | master for features / 3.4 up to 4.2 for bug fixes | Bug fix? | yes/no | New feature? | yes/no | BC breaks? | no diff --git a/CHANGELOG-4.1.md b/CHANGELOG-4.1.md index 60b848a532545..ad6a91cdd0b1b 100644 --- a/CHANGELOG-4.1.md +++ b/CHANGELOG-4.1.md @@ -7,6 +7,49 @@ in 4.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/v4.1.0...v4.1.1 +* 4.1.8 (2018-11-26) + + * bug #29318 [Console] Move back root exception to stack trace in verbose mode (chalasr) + * bug #29332 [PropertyAccess] make cache keys encoding bijective (nicolas-grekas) + * bug #29298 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) + * bug #29297 [Routing] fix trailing slash redirection when using RedirectableUrlMatcher (nicolas-grekas) + * bug #29313 [PropertyAccessor] fix encoding of cache keys (nicolas-grekas) + * bug #28917 [DoctrineBridge] catch errors while converting to db values in data collector (alekitto) + * bug #29317 [WebProfiler] Detect non-file paths in file viewer (ro0NL) + * bug #29305 [EventDispatcher] Unwrap wrapped listeners internally (ro0NL) + * bug #27314 [DoctrineBridge] fix case sensitivity issue in RememberMe\DoctrineTokenProvider (PF4Public) + * bug #29310 [MonologBridge] Return empty list for unknown requests (ro0NL) + * bug #29308 [Translation] Use XLIFF source rather than resname when there's no target (thewilkybarkid) + * bug #26244 [BrowserKit] fixed BC Break for HTTP_HOST header (brizzz) + * bug #28147 [DomCrawler] exclude fields inside "template" tags (Gorjunov) + * bug #29222 [Dotenv] properly parse backslashes in unquoted env vars (xabbuh) + * bug #29256 [HttpFoundation] Fixed absolute Request URI with default port (thomasbisignani) + * bug #29274 [Routing] Remove duplicate schemes and methods for invokable controllers (claudusd) + * bug #29271 [HttpFoundation] Fix trailing space for mime-type with parameters (Sascha Dens) + * bug #29243 [Cache] fix optimizing Psr6Cache for AdapterInterface pools (nicolas-grekas) + * bug #29247 [DI] fix taking lazy services into account when dumping the container (nicolas-grekas) + * bug #29249 [Form] Fixed empty data for compound date interval (HeahDude) + * bug #29265 [Bridge/PhpUnit] Use composer to download phpunit (nicolas-grekas) + * bug #28769 [FrameworkBundle] deal with explicitly enabled workflow nodes (xabbuh) + * bug #29223 [Validator] Added the missing constraints instance checks (thomasbisignani) + * bug #28966 [PropertyAccessor] Fix unable to write to singular property using setter while plural adder/remover exist (karser) + * bug #29182 [Form] Fixed empty data for compound date types (HeahDude) + * bug #29191 [Routing] generate(null) should throw an exception (nicolas-grekas) + * bug #29185 [Form] Fixed keeping hash of equal \DateTimeInterface on submit (HeahDude) + * bug #29141 [Workflow] Fixed bug of buildTransitionBlockerList when many transition are enabled (Tetragramat, lyrixx) + * bug #29137 [Workflow][FrameworkBundle] fixed guard event names for transitions (destillat, lyrixx) + * bug #28731 [Form] invalidate forms on transformation failures (xabbuh) + * bug #29152 [Config] Unset key during normalization (ro0NL) + * bug #29165 [DI] align IniFileLoader to PHP bugfix #76965 (nicolas-grekas) + * bug #29115 Change button_widget class to btn-primary (neFAST) + * bug #29131 [Dotenv] dont use getenv() to read SYMFONY_DOTENV_VARS (nicolas-grekas) + * bug #29057 [HttpFoundation] replace any preexisting Content-Type headers (nicolas-grekas) + * bug #29076 [Serializer] Allow null values when denormalizing with constructor missing data (danut007ro) + * bug #29104 [DI] fix dumping inlined services (nicolas-grekas) + * bug #29054 [VarDumper] fix dump of closures created from callables (nicolas-grekas) + * bug #29102 [DI] fix GraphvizDumper ignoring inline definitions (nicolas-grekas) + * bug #29107 [DI] dont track classes/interfaces used to compute autowiring error messages (nicolas-grekas) + * 4.1.7 (2018-11-03) * bug #28820 [DependencyInjection] Fix tags on multiple decorated service (Soner Sayakci) diff --git a/CHANGELOG-4.2.md b/CHANGELOG-4.2.md index d273a257a51dd..5e748a7913537 100644 --- a/CHANGELOG-4.2.md +++ b/CHANGELOG-4.2.md @@ -7,6 +7,16 @@ in 4.2 minor versions. To get the diff for a specific change, go to https://github.com/symfony/symfony/commit/XXX where XXX is the change hash To get the diff between two versions, go to https://github.com/symfony/symfony/compare/v4.2.0...v4.2.1 +* 4.2.0 (2018-11-30) + + * bug #29343 [Form] Handle all case variants of "nan" when parsing a number (mwhudson, xabbuh) + * bug #29373 [Routing] fix trailing slash redirection (nicolas-grekas) + * bug #29355 [PropertyAccess] calculate cache keys for property setters depending on the value (xabbuh) + * bug #29369 [DI] fix combinatorial explosion when analyzing the service graph (nicolas-grekas) + * bug #29349 [Debug] workaround opcache bug mutating "$this" !?! (nicolas-grekas) + * bug #29344 Fixes sprintf(): Too few arguments in Translator (stephanedelprat) + * bug #29318 [Console] Move back root exception to stack trace in verbose mode (chalasr) + * 4.2.0-RC1 (2018-11-26) * bug #29332 [PropertyAccess] make cache keys encoding bijective (nicolas-grekas) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 969c6a424cefc..1c329f2146c3e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -14,9 +14,9 @@ Symfony is the result of the work of many people who made the code better - Victor Berchet (victor) - Robin Chalas (chalas_r) - Kévin Dunglas (dunglas) + - Maxime Steinhausser (ogizanagi) - Jakub Zalas (jakubzalas) - Johannes S (johannes) - - Maxime Steinhausser (ogizanagi) - Kris Wallsmith (kriswallsmith) - Ryan Weaver (weaverryan) - Javier Eguiluz (javier.eguiluz) @@ -37,8 +37,8 @@ Symfony is the result of the work of many people who made the code better - Benjamin Eberlei (beberlei) - Igor Wiedler (igorw) - Jules Pietri (heah) - - Eriksen Costa (eriksencosta) - Yonel Ceruto (yonelceruto) + - Eriksen Costa (eriksencosta) - Guilhem Niot (energetick) - Sarah Khalil (saro0h) - Jonathan Wage (jwage) @@ -70,6 +70,7 @@ Symfony is the result of the work of many people who made the code better - Gábor Egyed (1ed) - Mathieu Piot (mpiot) - Titouan Galopin (tgalopin) + - Vladimir Reznichenko (kalessil) - Michel Weimerskirch (mweimerskirch) - Andrej Hudec (pulzarraider) - Konstantin Myakshin (koc) @@ -77,7 +78,6 @@ Symfony is the result of the work of many people who made the code better - Jáchym Toušek (enumag) - Charles Sarrazin (csarrazi) - David Maicher (dmaicher) - - Vladimir Reznichenko (kalessil) - Christian Raue - Issei Murasawa (issei_m) - Arnout Boks (aboks) @@ -86,13 +86,13 @@ Symfony is the result of the work of many people who made the code better - Dariusz Górecki (canni) - Douglas Greenshields (shieldo) - Dariusz Ruminski + - Grégoire Paris (greg0ire) - Lee McDermott - Brandon Turner - Luis Cordova (cordoval) - Graham Campbell (graham) - Daniel Holmes (dholmes) - Toni Uebernickel (havvg) - - Grégoire Paris (greg0ire) - Bart van den Burg (burgov) - Jordan Alliot (jalliot) - Jérôme Tamarelle (gromnan) @@ -113,12 +113,12 @@ Symfony is the result of the work of many people who made the code better - lenar - Alexander Schwenn (xelaris) - Włodzimierz Gajda (gajdaw) + - Tomáš Votruba (tomas_votruba) - Peter Kokot (maastermedia) - Jacob Dreesen (jdreesen) - Florian Voutzinos (florianv) - Colin Frei - Adrien Brault (adrienbrault) - - Tomáš Votruba (tomas_votruba) - Joshua Thijssen - excelwebzone - Gordon Franke (gimler) @@ -179,6 +179,7 @@ Symfony is the result of the work of many people who made the code better - jeremyFreeAgent (Jérémy Romey) (jeremyfreeagent) - James Halsall (jaitsu) - Matthieu Napoli (mnapoli) + - Florent Mata (fmata) - Warnar Boekkooi (boekkooi) - Alessandro Chitolina (alekitto) - Dmitrii Chekaliuk (lazyhammer) @@ -193,7 +194,6 @@ Symfony is the result of the work of many people who made the code better - Dennis Benkert (denderello) - DQNEO - Benjamin Dulau (dbenjamin) - - Florent Mata (fmata) - Mathieu Lemoine (lemoinem) - Thomas Calvet (fancyweb) - Christian Schmidt @@ -258,6 +258,7 @@ Symfony is the result of the work of many people who made the code better - Benoît Burnichon (bburnichon) - Roman Marintšenko (inori) - Xavier Montaña Carreras (xmontana) + - François-Xavier de Guillebon (de-gui_f) - Mickaël Andrieu (mickaelandrieu) - Maxime Veber (nek-) - Xavier Perez @@ -294,7 +295,9 @@ Symfony is the result of the work of many people who made the code better - Thomas Lallement (raziel057) - mcfedr (mcfedr) - Colin O'Dell (colinodell) + - Fabien Bourigault (fbourigault) - Giorgio Premi + - Jan Schädlich (jschaedl) - Beau Simensen (simensen) - Michael Hirschler (mvhirsch) - Robert Kiss (kepten) @@ -305,8 +308,8 @@ Symfony is the result of the work of many people who made the code better - Jérôme Parmentier (lctrs) - Michael Babker (mbabker) - Peter Kruithof (pkruithof) - - François-Xavier de Guillebon (de-gui_f) - Michael Holm (hollo) + - Remon van de Kamp (rpkamp) - Marc Weistroff (futurecat) - Christian Schmidt - MatTheCat @@ -350,8 +353,6 @@ Symfony is the result of the work of many people who made the code better - Ricard Clau (ricardclau) - Mark Challoner (markchalloner) - Gennady Telegin (gtelegin) - - Jan Schädlich (jschaedl) - - Fabien Bourigault (fbourigault) - Ben Davies (bendavies) - Erin Millard - Artur Melo (restless) @@ -445,6 +446,7 @@ Symfony is the result of the work of many people who made the code better - lancergr - Zan Baldwin - Mihai Stancu + - Ivan Nikolaev (destillat) - Olivier Dolbeau (odolbeau) - Jan Rosier (rosier) - Alessandro Lai (jean85) @@ -461,7 +463,6 @@ Symfony is the result of the work of many people who made the code better - Boris Vujicic (boris.vujicic) - Chris Sedlmayr (catchamonkey) - Mateusz Sip (mateusz_sip) - - Remon van de Kamp - Kamil Kokot (pamil) - Seb Koelen - Christoph Mewes (xrstf) @@ -470,6 +471,7 @@ Symfony is the result of the work of many people who made the code better - Dirk Pahl (dirkaholic) - cedric lombardot (cedriclombardot) - Jonas Flodén (flojon) + - Gonzalo Vilaseca (gonzalovilaseca) - Marcin Sikoń (marphi) - Dominik Zogg (dominik.zogg) - Marek Pietrzak @@ -643,7 +645,6 @@ Symfony is the result of the work of many people who made the code better - adev - Stefan Warman - Arkadius Stefanski (arkadius) - - Gonzalo Vilaseca (gonzalovilaseca) - Tristan Maindron (tmaindron) - Wesley Lancel - Ke WANG (yktd26) @@ -665,6 +666,7 @@ Symfony is the result of the work of many people who made the code better - Sergey (upyx) - Michael Devery (mickadoo) - Antoine Corcy + - Dmitrii Poddubnyi (karser) - Sascha Grossenbacher - Szijarto Tamas - Robin Lehrmann (robinlehrmann) @@ -804,7 +806,6 @@ Symfony is the result of the work of many people who made the code better - Sofiane HADDAG (sofhad) - frost-nzcr4 - Bozhidar Hristov - - Ivan Nikolaev (destillat) - Laurent Bassin (lbassin) - andrey1s - Abhoryo @@ -989,6 +990,7 @@ Symfony is the result of the work of many people who made the code better - Mathieu Santostefano - Arjan Keeman - Máximo Cuadros (mcuadros) + - Lukas Mencl - tamirvs - julien.galenski - Christian Neff @@ -1290,6 +1292,7 @@ Symfony is the result of the work of many people who made the code better - Adrien Samson (adriensamson) - Samuel Gordalina (gordalina) - Max Romanovsky (maxromanovsky) + - Nicolas Eeckeloo (neeckeloo) - Mathieu Morlon - Daniel Tschinder - Arnaud CHASSEUX @@ -1351,6 +1354,7 @@ Symfony is the result of the work of many people who made the code better - Andrew (drew) - kor3k kor3k (kor3k) - Stelian Mocanita (stelian) + - Thomas Bisignani (toma) - Justin (wackymole) - Flavian (2much) - Gautier Deuette @@ -1448,6 +1452,7 @@ Symfony is the result of the work of many people who made the code better - Phobetor - Andreas - Markus + - Daniel Gorgan - Thomas Chmielowiec - shdev - Andrey Ryaguzov @@ -1499,6 +1504,7 @@ Symfony is the result of the work of many people who made the code better - David Barratt - Pavel.Batanov - avi123 + - Pavel Prischepa - alsar - downace - Aarón Nieves Fernández @@ -1613,6 +1619,7 @@ Symfony is the result of the work of many people who made the code better - David Zuelke - Adrian - Oleg Andreyev + - neFAST - Pierre Rineau - Maxim Lovchikov - adenkejawen @@ -1710,7 +1717,6 @@ Symfony is the result of the work of many people who made the code better - Giovanni Albero (johntree) - Jorge Martin (jorgemartind) - Joeri Verdeyen (jverdeyen) - - Dmitrii Poddubnyi (karser) - Kevin Verschaeve (keversc) - Kevin Herrera (kherge) - Luis Ramón López López (lrlopez) @@ -1872,6 +1878,7 @@ Symfony is the result of the work of many people who made the code better - zorn - Yuriy Potemkin - Emilie Lorenzo + - Edvin Hultberg - Benjamin Long - Matt Janssen - Ben Miller @@ -1992,6 +1999,7 @@ Symfony is the result of the work of many people who made the code better - Alex Carol (picard89) - Daniel Perez Pinazo (pitiflautico) - Phil Taylor (prazgod) + - Maxim Pustynnikov (pustynnikov) - Brayden Williams (redstar504) - Rich Sage (richsage) - Rokas Mikalkėnas (rokasm) diff --git a/UPGRADE-4.2.md b/UPGRADE-4.2.md index bdf2ab0cececf..a2fadabc95b14 100644 --- a/UPGRADE-4.2.md +++ b/UPGRADE-4.2.md @@ -199,7 +199,10 @@ Messenger --------- * The `MiddlewareInterface::handle()` and `SenderInterface::send()` methods must now return an `Envelope` instance. - * The return value of handlers is ignored. If you used to return a value, e.g in query bus handlers, you can either: + * The return value of handlers isn't forwarded anymore by middleware and buses. + If you used to return a value, e.g in query bus handlers, you can either: + - get the result from the `HandledStamp` in the envelope returned by the bus. + - use the `HandleTrait` to leverage a message bus, expecting a single, synchronous message handling and returning its result. - make your `Query` mutable to allow setting & getting a result: ```php // When dispatching: @@ -209,15 +212,6 @@ Messenger // In your handler: $query->setResult($yourResult); ``` - - define a callable on your `Query` to be called in your handler: - ```php - // When dispatching: - $bus->dispatch(new Query([$this, 'onResult'])); - - // In your handler: - $query->executeCallback($yourResult); - ``` - * The `EnvelopeAwareInterface` was removed and the `MiddlewareInterface::handle()` method now requires an `Envelope` object as first argument. When using built-in middleware with the provided `MessageBus`, you will not have to do anything. If you use your own `MessageBusInterface` implementation, you must wrap the message in an `Envelope` before passing it to middleware. diff --git a/phpunit b/phpunit index f4b80ed064121..9975195309a81 100755 --- a/phpunit +++ b/phpunit @@ -1,7 +1,7 @@ #!/usr/bin/env php getTrace(); + array_unshift($trace, array( + 'function' => '', + 'file' => $e->getFile() ?: 'n/a', + 'line' => $e->getLine() ?: 'n/a', + 'args' => array(), + )); + for ($i = 0, $count = \count($trace); $i < $count; ++$i) { $class = isset($trace[$i]['class']) ? $trace[$i]['class'] : ''; $type = isset($trace[$i]['type']) ? $trace[$i]['type'] : ''; diff --git a/src/Symfony/Component/Console/Tests/ApplicationTest.php b/src/Symfony/Component/Console/Tests/ApplicationTest.php index f14a051fc797e..d7b0451efc9d4 100644 --- a/src/Symfony/Component/Console/Tests/ApplicationTest.php +++ b/src/Symfony/Component/Console/Tests/ApplicationTest.php @@ -849,6 +849,31 @@ public function testRenderAnonymousException() $this->assertContains('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true)); } + public function testRenderExceptionStackTraceContainsRootException() + { + $application = new Application(); + $application->setAutoExit(false); + $application->register('foo')->setCode(function () { + throw new class('') extends \InvalidArgumentException { + }; + }); + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'foo'), array('decorated' => false)); + $this->assertContains('[InvalidArgumentException@anonymous]', $tester->getDisplay(true)); + + $application = new Application(); + $application->setAutoExit(false); + $application->register('foo')->setCode(function () { + throw new \InvalidArgumentException(sprintf('Dummy type "%s" is invalid.', \get_class(new class() { + }))); + }); + $tester = new ApplicationTester($application); + + $tester->run(array('command' => 'foo'), array('decorated' => false)); + $this->assertContains('Dummy type "@anonymous" is invalid.', $tester->getDisplay(true)); + } + public function testRun() { $application = new Application(); diff --git a/src/Symfony/Component/Debug/DebugClassLoader.php b/src/Symfony/Component/Debug/DebugClassLoader.php index 5c2be6b3b4331..6d5fc9c2c3e85 100644 --- a/src/Symfony/Component/Debug/DebugClassLoader.php +++ b/src/Symfony/Component/Debug/DebugClassLoader.php @@ -141,14 +141,14 @@ public function loadClass($class) try { if ($this->isFinder && !isset($this->loaded[$class])) { $this->loaded[$class] = true; - if ($file = $this->classLoader[0]->findFile($class) ?: false) { - $wasCached = \function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file); - + if (!$file = $this->classLoader[0]->findFile($class) ?: false) { + // no-op + } elseif (\function_exists('opcache_is_script_cached') && @opcache_is_script_cached($file)) { require $file; - if ($wasCached) { - return; - } + return; + } else { + require $file; } } else { \call_user_func($this->classLoader, $class); diff --git a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php index 6049b3eefddc5..d4766a16ec0b2 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/InlineServiceDefinitionsPass.php @@ -191,7 +191,9 @@ private function isInlineableDefinition($id, Definition $definition) $srcIds = array(); $srcCount = 0; + $isReferencedByConstructor = false; foreach ($this->graph->getNode($id)->getInEdges() as $edge) { + $isReferencedByConstructor = $isReferencedByConstructor || $edge->isReferencedByConstructor(); $srcId = $edge->getSourceNode()->getId(); $this->connectedIds[$srcId] = true; if ($edge->isWeak()) { @@ -211,6 +213,10 @@ private function isInlineableDefinition($id, Definition $definition) return false; } + if ($isReferencedByConstructor && $this->container->getDefinition($srcId)->isLazy() && ($definition->getProperties() || $definition->getMethodCalls() || $definition->getConfigurator())) { + return false; + } + return $this->container->getDefinition($srcId)->isShared(); } } diff --git a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php index 1baec0ebdf013..204a991850b74 100644 --- a/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php +++ b/src/Symfony/Component/DependencyInjection/Dumper/PhpDumper.php @@ -171,21 +171,22 @@ public function dump(array $options = array()) } (new AnalyzeServiceReferencesPass(false, !$this->getProxyDumper() instanceof NullDumper))->process($this->container); + $checkedNodes = array(); $this->circularReferences = array(); $this->singleUsePrivateIds = array(); - foreach (array(true, false) as $byConstructor) { - foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { - if (!$node->getValue() instanceof Definition) { - continue; - } - $currentPath = array($id => $id); - $this->analyzeCircularReferences($node->getOutEdges(), $currentPath, $id, $byConstructor); - if ($this->isSingleUsePrivateNode($node)) { - $this->singleUsePrivateIds[$id] = $id; - } + foreach ($this->container->getCompiler()->getServiceReferenceGraph()->getNodes() as $id => $node) { + if (!$node->getValue() instanceof Definition) { + continue; + } + if (!isset($checkedNodes[$id])) { + $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes); + } + if ($this->isSingleUsePrivateNode($node)) { + $this->singleUsePrivateIds[$id] = $id; } } $this->container->getCompiler()->getServiceReferenceGraph()->clear(); + $checkedNodes = array(); $this->docStar = $options['debug'] ? '*' : ''; @@ -337,12 +338,12 @@ private function getProxyDumper(): ProxyDumper return $this->proxyDumper; } - private function analyzeCircularReferences(array $edges, &$currentPath, $sourceId, $byConstructor) + private function analyzeCircularReferences($sourceId, array $edges, &$checkedNodes, &$currentPath = array()) { + $checkedNodes[$sourceId] = true; + $currentPath[$sourceId] = $sourceId; + foreach ($edges as $edge) { - if ($byConstructor && !$edge->isReferencedByConstructor()) { - continue; - } $node = $edge->getDestNode(); $id = $node->getId(); @@ -351,20 +352,42 @@ private function analyzeCircularReferences(array $edges, &$currentPath, $sourceI } elseif (isset($currentPath[$id])) { $currentId = $id; foreach (array_reverse($currentPath) as $parentId) { - if (!isset($this->circularReferences[$parentId][$currentId])) { - $this->circularReferences[$parentId][$currentId] = $byConstructor; + $this->circularReferences[$parentId][$currentId] = $currentId; + if ($parentId === $id) { + break; } + $currentId = $parentId; + } + } elseif (!isset($checkedNodes[$id])) { + $this->analyzeCircularReferences($id, $node->getOutEdges(), $checkedNodes, $currentPath); + } elseif (isset($this->circularReferences[$id])) { + $this->connectCircularReferences($id, $currentPath); + } + } + unset($currentPath[$sourceId]); + } + + private function connectCircularReferences($sourceId, &$currentPath, &$subPath = array()) + { + $subPath[$sourceId] = $sourceId; + $currentPath[$sourceId] = $sourceId; + + foreach ($this->circularReferences[$sourceId] as $id) { + if (isset($currentPath[$id])) { + $currentId = $id; + foreach (array_reverse($currentPath) as $parentId) { + $this->circularReferences[$parentId][$currentId] = $currentId; if ($parentId === $id) { break; } $currentId = $parentId; } - } else { - $currentPath[$id] = $id; - $this->analyzeCircularReferences($node->getOutEdges(), $currentPath, $id, $byConstructor); - unset($currentPath[$id]); + } elseif (!isset($subPath[$id]) && isset($this->circularReferences[$id])) { + $this->connectCircularReferences($id, $currentPath, $subPath); } } + unset($currentPath[$sourceId]); + unset($subPath[$sourceId]); } private function collectLineage($class, array &$lineage) @@ -572,8 +595,11 @@ private function addServiceConfigurator(Definition $definition, string $variable if (\is_array($callable)) { if ($callable[0] instanceof Reference - || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))) { - return sprintf(" %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName); + || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0])) + ) { + $callable[0] = $this->dumpValue($callable[0]); + + return sprintf(' '.('$' === $callable[0][0] ? '%s' : '(%s)')."->%s(\$%s);\n", $callable[0], $callable[1], $variableName); } $class = $this->dumpValue($callable[0]); @@ -719,7 +745,7 @@ private function addInlineReference(string $id, Definition $definition, string $ $hasSelfRef = isset($this->circularReferences[$id][$targetId]); $forConstructor = $forConstructor && !isset($this->definitionVariables[$definition]); - $code = $hasSelfRef && $this->circularReferences[$id][$targetId] && !$forConstructor ? $this->addInlineService($id, $definition, $definition) : ''; + $code = $hasSelfRef && !$forConstructor ? $this->addInlineService($id, $definition, $definition) : ''; if (isset($this->referenceVariables[$targetId]) || (2 > $callCount && (!$hasSelfRef || !$forConstructor))) { return $code; diff --git a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php index 75502db0e9f56..95f44c86c7567 100644 --- a/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php +++ b/src/Symfony/Component/Form/Extension/Core/DataTransformer/NumberToLocalizedStringTransformer.php @@ -146,7 +146,7 @@ public function reverseTransform($value) return; } - if ('NaN' === $value) { + if (\in_array($value, array('NaN', 'NAN', 'nan'), true)) { throw new TransformationFailedException('"NaN" is not a valid number'); } diff --git a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php index d6a4662102232..176d3a9a58f9e 100644 --- a/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php +++ b/src/Symfony/Component/Form/Tests/Extension/Core/DataTransformer/NumberToLocalizedStringTransformerTest.php @@ -514,24 +514,24 @@ public function testReverseTransformExpectsValidNumber() /** * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException + * @dataProvider nanRepresentationProvider * * @see https://github.com/symfony/symfony/issues/3161 */ - public function testReverseTransformDisallowsNaN() + public function testReverseTransformDisallowsNaN($nan) { $transformer = new NumberToLocalizedStringTransformer(); - $transformer->reverseTransform('NaN'); + $transformer->reverseTransform($nan); } - /** - * @expectedException \Symfony\Component\Form\Exception\TransformationFailedException - */ - public function testReverseTransformDisallowsNaN2() + public function nanRepresentationProvider() { - $transformer = new NumberToLocalizedStringTransformer(); - - $transformer->reverseTransform('nan'); + return array( + array('nan'), + array('NaN'), // see https://github.com/symfony/symfony/issues/3161 + array('NAN'), + ); } /** diff --git a/src/Symfony/Component/HttpKernel/Kernel.php b/src/Symfony/Component/HttpKernel/Kernel.php index 7116b7015c465..225a047d141f3 100644 --- a/src/Symfony/Component/HttpKernel/Kernel.php +++ b/src/Symfony/Component/HttpKernel/Kernel.php @@ -73,12 +73,12 @@ abstract class Kernel implements KernelInterface, RebootableInterface, Terminabl private $requestStackSize = 0; private $resetServices = false; - const VERSION = '4.2.0-DEV'; + const VERSION = '4.2.0'; const VERSION_ID = 40200; const MAJOR_VERSION = 4; const MINOR_VERSION = 2; const RELEASE_VERSION = 0; - const EXTRA_VERSION = 'DEV'; + const EXTRA_VERSION = ''; const END_OF_MAINTENANCE = '07/2019'; const END_OF_LIFE = '01/2020'; diff --git a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php index f3915d44c00a5..40f10a54725b5 100644 --- a/src/Symfony/Component/PropertyAccess/PropertyAccessor.php +++ b/src/Symfony/Component/PropertyAccess/PropertyAccessor.php @@ -587,7 +587,8 @@ private function writeCollection($zval, $property, $collection, $addMethod, $rem */ private function getWriteAccessInfo(string $class, string $property, $value): array { - $key = str_replace('\\', '.', $class).'..'.$property; + $useAdderAndRemover = \is_array($value) || $value instanceof \Traversable; + $key = str_replace('\\', '.', $class).'..'.$property.'..'.(int) $useAdderAndRemover; if (isset($this->writePropertyCache[$key])) { return $this->writePropertyCache[$key]; @@ -607,6 +608,16 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar $camelized = $this->camelize($property); $singulars = (array) Inflector::singularize($camelized); + if ($useAdderAndRemover) { + $methods = $this->findAdderAndRemover($reflClass, $singulars); + + if (null !== $methods) { + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; + $access[self::ACCESS_ADDER] = $methods[0]; + $access[self::ACCESS_REMOVER] = $methods[1]; + } + } + if (!isset($access[self::ACCESS_TYPE])) { $setter = 'set'.$camelized; $getsetter = lcfirst($camelized); // jQuery style, e.g. read: last(), write: last($item) @@ -628,22 +639,16 @@ private function getWriteAccessInfo(string $class, string $property, $value): ar $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_MAGIC; $access[self::ACCESS_NAME] = $setter; } elseif (null !== $methods = $this->findAdderAndRemover($reflClass, $singulars)) { - if (\is_array($value) || $value instanceof \Traversable) { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_ADDER_AND_REMOVER; - $access[self::ACCESS_ADDER] = $methods[0]; - $access[self::ACCESS_REMOVER] = $methods[1]; - } else { - $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; - $access[self::ACCESS_NAME] = sprintf( - 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. - 'the new value must be an array or an instance of \Traversable, '. - '"%s" given.', - $property, - $reflClass->name, - implode('()", "', $methods), - \is_object($value) ? \get_class($value) : \gettype($value) - ); - } + $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; + $access[self::ACCESS_NAME] = sprintf( + 'The property "%s" in class "%s" can be defined with the methods "%s()" but '. + 'the new value must be an array or an instance of \Traversable, '. + '"%s" given.', + $property, + $reflClass->name, + implode('()", "', $methods), + \is_object($value) ? \get_class($value) : \gettype($value) + ); } else { $access[self::ACCESS_TYPE] = self::ACCESS_TYPE_NOT_FOUND; $access[self::ACCESS_NAME] = sprintf( @@ -680,6 +685,18 @@ private function isPropertyWritable($object, string $property): bool $access = $this->getWriteAccessInfo(\get_class($object), $property, array()); + $isWritable = self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE] + || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE] + || (!$access[self::ACCESS_HAS_PROPERTY] && property_exists($object, $property)) + || self::ACCESS_TYPE_MAGIC === $access[self::ACCESS_TYPE]; + + if ($isWritable) { + return true; + } + + $access = $this->getWriteAccessInfo(\get_class($object), $property, ''); + return self::ACCESS_TYPE_METHOD === $access[self::ACCESS_TYPE] || self::ACCESS_TYPE_PROPERTY === $access[self::ACCESS_TYPE] || self::ACCESS_TYPE_ADDER_AND_REMOVER === $access[self::ACCESS_TYPE] diff --git a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php index 8ae36059c0d2a..3f2bb4e9eaaaf 100644 --- a/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php +++ b/src/Symfony/Component/PropertyAccess/Tests/PropertyAccessorTest.php @@ -708,7 +708,27 @@ public function testWriteToPluralPropertyWhileSingularOneExists() $this->propertyAccessor->isWritable($object, 'emails'); //cache access info $this->propertyAccessor->setValue($object, 'emails', array('test@email.com')); - self::assertEquals(array('test@email.com'), $object->getEmails()); - self::assertNull($object->getEmail()); + $this->assertEquals(array('test@email.com'), $object->getEmails()); + $this->assertNull($object->getEmail()); + } + + public function testAdderAndRemoverArePreferredOverSetter() + { + $object = new TestPluralAdderRemoverAndSetter(); + + $this->propertyAccessor->isWritable($object, 'emails'); //cache access info + $this->propertyAccessor->setValue($object, 'emails', array('test@email.com')); + + $this->assertEquals(array('test@email.com'), $object->getEmails()); + } + + public function testAdderAndRemoverArePreferredOverSetterForSameSingularAndPlural() + { + $object = new TestPluralAdderRemoverAndSetterSameSingularAndPlural(); + + $this->propertyAccessor->isWritable($object, 'aircraft'); //cache access info + $this->propertyAccessor->setValue($object, 'aircraft', array('aeroplane')); + + $this->assertEquals(array('aeroplane'), $object->getAircraft()); } } diff --git a/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php new file mode 100644 index 0000000000000..ecb3f9b4a9d32 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetter.php @@ -0,0 +1,37 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\PropertyAccess\Tests; + +class TestPluralAdderRemoverAndSetter +{ + private $emails = array(); + + public function getEmails() + { + return $this->emails; + } + + public function setEmails(array $emails) + { + $this->emails = array('foo@email.com'); + } + + public function addEmail($email) + { + $this->emails[] = $email; + } + + public function removeEmail($email) + { + $this->emails = array_diff($this->emails, array($email)); + } +} diff --git a/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php new file mode 100644 index 0000000000000..bb3b4f4688dc5 --- /dev/null +++ b/src/Symfony/Component/PropertyAccess/Tests/TestPluralAdderRemoverAndSetterSameSingularAndPlural.php @@ -0,0 +1,28 @@ +aircraft; + } + + public function setAircraft(array $aircraft) + { + $this->aircraft = array('plane'); + } + + public function addAircraft($aircraft) + { + $this->aircraft[] = $aircraft; + } + + public function removeAircraft($aircraft) + { + $this->aircraft = array_diff($this->aircraft, array($aircraft)); + } +} diff --git a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherTrait.php b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherTrait.php index 8c7241e20b588..774f503ac7f13 100644 --- a/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherTrait.php +++ b/src/Symfony/Component/Routing/Matcher/Dumper/PhpMatcherTrait.php @@ -130,7 +130,7 @@ private function doMatch(string $rawPathinfo, array &$allow = array(), array &$a continue; } - if ('/' === $pathinfo || $hasTrailingSlash === ('/' === $pathinfo[-1])) { + if ('/' === $pathinfo || (!$hasTrailingSlash ? '/' !== $pathinfo[-1] || !preg_match($regex, substr($pathinfo, 0, -1), $n) || $m !== (int) $n['MARK'] : '/' === $pathinfo[-1])) { // no-op } elseif ($this instanceof RedirectableUrlMatcherInterface) { return null; diff --git a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php index e5ee5c5b53c6e..32bf4ed82d187 100644 --- a/src/Symfony/Component/Routing/Matcher/UrlMatcher.php +++ b/src/Symfony/Component/Routing/Matcher/UrlMatcher.php @@ -160,8 +160,13 @@ protected function matchCollection($pathinfo, RouteCollection $routes) continue; } - if ($supportsTrailingSlash && $hasTrailingSlash !== ('/' === $pathinfo[-1])) { - return; + if ($supportsTrailingSlash) { + if (!$hasTrailingSlash && '/' === $pathinfo[-1] && preg_match($regex, substr($pathinfo, 0, -1))) { + return; + } + if ($hasTrailingSlash && '/' !== $pathinfo[-1]) { + return; + } } $hostMatches = array(); diff --git a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php index f1b4314455f50..5f24e5f794ce3 100644 --- a/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php +++ b/src/Symfony/Component/Routing/Tests/Matcher/UrlMatcherTest.php @@ -683,6 +683,15 @@ public function testHostWithDot() $this->assertEquals('b', $matcher->match('/bar/abc.123')['_route']); } + public function testSlashVariant() + { + $coll = new RouteCollection(); + $coll->add('a', new Route('/foo/{bar}', array(), array('bar' => '.*'))); + + $matcher = $this->getUrlMatcher($coll); + $this->assertEquals('a', $matcher->match('/foo/')['_route']); + } + protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) { return new UrlMatcher($routes, $context ?: new RequestContext()); diff --git a/src/Symfony/Component/Translation/Translator.php b/src/Symfony/Component/Translation/Translator.php index 864db835c77a3..7fcb77853d5e1 100644 --- a/src/Symfony/Component/Translation/Translator.php +++ b/src/Symfony/Component/Translation/Translator.php @@ -226,7 +226,7 @@ public function trans($id, array $parameters = array(), $domain = null, $locale */ public function transChoice($id, $number, array $parameters = array(), $domain = null, $locale = null) { - @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%count%" parameter.', __METHOD__), E_USER_DEPRECATED); + @trigger_error(sprintf('The "%s()" method is deprecated since Symfony 4.2, use the trans() one instead with a "%%count%%" parameter.', __METHOD__), E_USER_DEPRECATED); if (!$this->formatter instanceof ChoiceMessageFormatterInterface) { throw new LogicException(sprintf('The formatter "%s" does not support plural translations.', \get_class($this->formatter))); diff --git a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php index 544c82f6c074b..e4f7df1757da6 100644 --- a/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php +++ b/src/Symfony/Component/Validator/Context/ExecutionContextInterface.php @@ -112,7 +112,7 @@ public function getValidator(); * Returns the currently validated object. * * If the validator is currently validating a class constraint, the - * object of that class is returned. If it is a validating a property or + * object of that class is returned. If it is validating a property or * getter constraint, the object that the property/getter belongs to is * returned. *
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: