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. *