diff --git a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig index 819711eff7..983a6f39f0 100644 --- a/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig +++ b/src/Symfony/Bundle/WebProfilerBundle/Resources/views/Collector/mailer.html.twig @@ -94,7 +94,7 @@ {% for transport in events.transports %}
{{ events.messages(transport)|length }} - {{ events.messages(transport)|length == 1 ? 'message' : 'messages' }} + {{ events.messages(transport)|length == 1 ? 'message' : 'messages' }}
{% endfor %} diff --git a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php index da909ae489..7e76064ce6 100644 --- a/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php +++ b/src/Symfony/Component/DependencyInjection/Compiler/DecoratorServicePass.php @@ -78,8 +78,18 @@ class DecoratorServicePass implements CompilerPassInterface if (isset($decoratingDefinitions[$inner])) { $decoratingDefinition = $decoratingDefinitions[$inner]; - $definition->setTags(array_merge($decoratingDefinition->getTags(), $definition->getTags())); - $decoratingDefinition->setTags([]); + + $decoratingTags = $decoratingDefinition->getTags(); + $resetTags = []; + + if (isset($decoratingTags['container.service_locator'])) { + // container.service_locator has special logic and it must not be transferred out to decorators + $resetTags = ['container.service_locator' => $decoratingTags['container.service_locator']]; + unset($decoratingTags['container.service_locator']); + } + + $definition->setTags(array_merge($decoratingTags, $definition->getTags())); + $decoratingDefinition->setTags($resetTags); $decoratingDefinitions[$inner] = $definition; } diff --git a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php index fbd19ab3f4..ed111d6d2c 100644 --- a/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php +++ b/src/Symfony/Component/DependencyInjection/Tests/Compiler/DecoratorServicePassTest.php @@ -223,6 +223,25 @@ class DecoratorServicePassTest extends TestCase $this->assertEquals(['bar' => ['attr' => 'baz']], $container->getDefinition('deco2')->getTags()); } + public function testProcessLeavesServiceLocatorTagOnOriginalDefinition() + { + $container = new ContainerBuilder(); + $container + ->register('foo') + ->setTags(['container.service_locator' => [0 => []], 'bar' => ['attr' => 'baz']]) + ; + $container + ->register('baz') + ->setTags(['foobar' => ['attr' => 'bar']]) + ->setDecoratedService('foo') + ; + + $this->process($container); + + $this->assertEquals(['container.service_locator' => [0 => []]], $container->getDefinition('baz.inner')->getTags()); + $this->assertEquals(['bar' => ['attr' => 'baz'], 'foobar' => ['attr' => 'bar']], $container->getDefinition('baz')->getTags()); + } + protected function process(ContainerBuilder $container) { $pass = new DecoratorServicePass(); diff --git a/src/Symfony/Component/HttpClient/CurlHttpClient.php b/src/Symfony/Component/HttpClient/CurlHttpClient.php index a36372c619..b076e9a7b5 100644 --- a/src/Symfony/Component/HttpClient/CurlHttpClient.php +++ b/src/Symfony/Component/HttpClient/CurlHttpClient.php @@ -116,7 +116,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, $url = implode('', $url); if (!isset($options['normalized_headers']['user-agent'])) { - $options['normalized_headers']['user-agent'][] = $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl'; + $options['headers'][] = 'User-Agent: Symfony HttpClient/Curl'; } $curlopts = [ @@ -217,8 +217,8 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface, $curlopts[CURLOPT_NOSIGNAL] = true; } - if (!isset($options['normalized_headers']['accept-encoding']) && CURL_VERSION_LIBZ & self::$curlVersion['features']) { - $curlopts[CURLOPT_ENCODING] = 'gzip'; // Expose only one encoding, some servers mess up when more are provided + if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { + $options['headers'][] = 'Accept-Encoding: gzip'; // Expose only one encoding, some servers mess up when more are provided } foreach ($options['headers'] as $header) { diff --git a/src/Symfony/Component/HttpClient/NativeHttpClient.php b/src/Symfony/Component/HttpClient/NativeHttpClient.php index 9e95d1170e..d60f541527 100644 --- a/src/Symfony/Component/HttpClient/NativeHttpClient.php +++ b/src/Symfony/Component/HttpClient/NativeHttpClient.php @@ -77,7 +77,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $options['headers'][] = 'Content-Type: application/x-www-form-urlencoded'; } - if ($gzipEnabled = \extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { + if (\extension_loaded('zlib') && !isset($options['normalized_headers']['accept-encoding'])) { // gzip is the most widely available algo, no need to deal with deflate $options['headers'][] = 'Accept-Encoding: gzip'; } @@ -227,7 +227,7 @@ final class NativeHttpClient implements HttpClientInterface, LoggerAwareInterfac $context = stream_context_create($context, ['notification' => $notification]); self::configureHeadersAndProxy($context, $host, $options['headers'], $proxy, $noProxy); - return new NativeResponse($this->multi, $context, implode('', $url), $options, $gzipEnabled, $info, $resolveRedirect, $onProgress, $this->logger); + return new NativeResponse($this->multi, $context, implode('', $url), $options, $info, $resolveRedirect, $onProgress, $this->logger); } /** diff --git a/src/Symfony/Component/HttpClient/Response/CurlResponse.php b/src/Symfony/Component/HttpClient/Response/CurlResponse.php index 6188587288..e1699b9b2b 100644 --- a/src/Symfony/Component/HttpClient/Response/CurlResponse.php +++ b/src/Symfony/Component/HttpClient/Response/CurlResponse.php @@ -52,6 +52,7 @@ final class CurlResponse implements ResponseInterface $this->id = $id = (int) $ch; $this->logger = $logger; + $this->shouldBuffer = $options['buffer'] ?? true; $this->timeout = $options['timeout'] ?? null; $this->info['http_method'] = $method; $this->info['user_data'] = $options['user_data'] ?? null; @@ -65,50 +66,25 @@ final class CurlResponse implements ResponseInterface curl_setopt($ch, CURLOPT_PRIVATE, \in_array($method, ['GET', 'HEAD', 'OPTIONS', 'TRACE'], true) && 1.0 < (float) ($options['http_version'] ?? 1.1) ? 'H2' : 'H0'); // H = headers + retry counter } - if (null === $content = &$this->content) { - $content = null === $options || true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - } else { - // Move the pushed response to the activity list - $buffer = $options['buffer']; - - if ('H' !== curl_getinfo($ch, CURLINFO_PRIVATE)[0]) { - if ($options['buffer'] instanceof \Closure) { - try { - [$content, $buffer] = [null, $content]; - [$content, $buffer] = [$buffer, $options['buffer']($headers)]; - } catch (\Throwable $e) { - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = $e; - [$content, $buffer] = [$buffer, false]; - } - } - - if (ftell($content)) { - rewind($content); - $multi->handlesActivity[$id][] = stream_get_contents($content); - } - } - - if (\is_resource($buffer)) { - $content = $buffer; - } elseif (true !== $buffer) { - $content = null; - } - } - - curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger, &$content): int { - return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger, $content); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, static function ($ch, string $data) use (&$info, &$headers, $options, $multi, $id, &$location, $resolveRedirect, $logger): int { + return self::parseHeaderLine($ch, $data, $info, $headers, $options, $multi, $id, $location, $resolveRedirect, $logger); }); if (null === $options) { // Pushed response: buffer until requested - curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content): int { - return fwrite($content, $data); + curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int { + $multi->handlesActivity[$id][] = $data; + curl_pause($ch, CURLPAUSE_RECV); + + return \strlen($data); }); return; } + $this->inflate = !isset($options['normalized_headers']['accept-encoding']); + curl_pause($ch, CURLPAUSE_CONT); + if ($onProgress = $options['on_progress']) { $url = isset($info['url']) ? ['url' => $info['url']] : []; curl_setopt($ch, CURLOPT_NOPROGRESS, false); @@ -128,33 +104,16 @@ final class CurlResponse implements ResponseInterface }); } - curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use (&$content, $multi, $id): int { + curl_setopt($ch, CURLOPT_WRITEFUNCTION, static function ($ch, string $data) use ($multi, $id): int { $multi->handlesActivity[$id][] = $data; - return null !== $content ? fwrite($content, $data) : \strlen($data); + return \strlen($data); }); $this->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - $waitFor = curl_getinfo($ch = $response->handle, CURLINFO_PRIVATE); - if ('H' === $waitFor[0] || 'D' === $waitFor[0]) { - try { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } catch (\Throwable $e) { - // Persist timeouts thrown during initialization - $response->info['error'] = $e->getMessage(); - $response->close(); - throw $e; - } - } + return 'H' === $waitFor[0] || 'D' === $waitFor[0]; }; // Schedule the request in a non-blocking way @@ -241,6 +200,7 @@ final class CurlResponse implements ResponseInterface */ private function close(): void { + $this->inflate = null; unset($this->multi->openHandles[$this->id], $this->multi->handlesActivity[$this->id]); curl_setopt($this->handle, CURLOPT_PRIVATE, '_0'); @@ -419,22 +379,6 @@ final class CurlResponse implements ResponseInterface } curl_setopt($ch, CURLOPT_PRIVATE, $waitFor); - - try { - if (!$content && $options['buffer'] instanceof \Closure && $content = $options['buffer']($headers) ?: null) { - $content = \is_resource($content) ? $content : fopen('php://temp', 'w+'); - } - - if (null !== $info['error']) { - throw new TransportException($info['error']); - } - } catch (\Throwable $e) { - $multi->handlesActivity[$id] = $multi->handlesActivity[$id] ?? [new FirstChunk()]; - $multi->handlesActivity[$id][] = null; - $multi->handlesActivity[$id][] = $e; - - return 0; - } } elseif (null !== $info['redirect_url'] && $logger) { $logger->info(sprintf('Redirecting: "%s %s"', $info['http_code'], $info['redirect_url'])); } diff --git a/src/Symfony/Component/HttpClient/Response/MockResponse.php b/src/Symfony/Component/HttpClient/Response/MockResponse.php index a420914958..b458665d44 100644 --- a/src/Symfony/Component/HttpClient/Response/MockResponse.php +++ b/src/Symfony/Component/HttpClient/Response/MockResponse.php @@ -94,6 +94,7 @@ class MockResponse implements ResponseInterface */ protected function close(): void { + $this->inflate = null; $this->body = []; } @@ -105,22 +106,9 @@ class MockResponse implements ResponseInterface $response = new self([]); $response->requestOptions = $options; $response->id = ++self::$idSequence; - - if (!($options['buffer'] ?? null) instanceof \Closure) { - $response->content = true === ($options['buffer'] ?? true) ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - } + $response->shouldBuffer = $options['buffer'] ?? true; $response->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - - if (\is_array($response->body[0] ?? null)) { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } + return \is_array($response->body[0] ?? null); }; $response->info['redirect_count'] = 0; @@ -198,11 +186,6 @@ class MockResponse implements ResponseInterface } else { // Data or timeout chunk $multi->handlesActivity[$id][] = $chunk; - - if (\is_string($chunk) && null !== $response->content) { - // Buffer response body - fwrite($response->content, $chunk); - } } } } diff --git a/src/Symfony/Component/HttpClient/Response/NativeResponse.php b/src/Symfony/Component/HttpClient/Response/NativeResponse.php index db0540d28b..7abe18abab 100644 --- a/src/Symfony/Component/HttpClient/Response/NativeResponse.php +++ b/src/Symfony/Component/HttpClient/Response/NativeResponse.php @@ -32,7 +32,6 @@ final class NativeResponse implements ResponseInterface private $onProgress; private $remaining; private $buffer; - private $inflate; private $multi; private $debugBuffer; private $shouldBuffer; @@ -40,7 +39,7 @@ final class NativeResponse implements ResponseInterface /** * @internal */ - public function __construct(NativeClientState $multi, $context, string $url, $options, bool $gzipEnabled, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger) + public function __construct(NativeClientState $multi, $context, string $url, array $options, array &$info, callable $resolveRedirect, ?callable $onProgress, ?LoggerInterface $logger) { $this->multi = $multi; $this->id = (int) $context; @@ -51,28 +50,17 @@ final class NativeResponse implements ResponseInterface $this->info = &$info; $this->resolveRedirect = $resolveRedirect; $this->onProgress = $onProgress; - $this->content = true === $options['buffer'] ? fopen('php://temp', 'w+') : (\is_resource($options['buffer']) ? $options['buffer'] : null); - $this->shouldBuffer = $options['buffer'] instanceof \Closure ? $options['buffer'] : null; + $this->inflate = !isset($options['normalized_headers']['accept-encoding']); + $this->shouldBuffer = $options['buffer'] ?? true; - // Temporary resources to dechunk/inflate the response stream + // Temporary resource to dechunk the response stream $this->buffer = fopen('php://temp', 'w+'); - $this->inflate = $gzipEnabled ? inflate_init(ZLIB_ENCODING_GZIP) : null; $info['user_data'] = $options['user_data']; ++$multi->responseCount; $this->initializer = static function (self $response) { - if (null !== $response->info['error']) { - throw new TransportException($response->info['error']); - } - - if (null === $response->remaining) { - foreach (self::stream([$response]) as $chunk) { - if ($chunk->isFirst()) { - break; - } - } - } + return null === $response->remaining; }; } @@ -169,7 +157,7 @@ final class NativeResponse implements ResponseInterface stream_set_blocking($h, false); $this->context = $this->resolveRedirect = null; - // Create dechunk and inflate buffers + // Create dechunk buffers if (isset($this->headers['content-length'])) { $this->remaining = (int) $this->headers['content-length'][0]; } elseif ('chunked' === ($this->headers['transfer-encoding'][0] ?? null)) { @@ -179,27 +167,6 @@ final class NativeResponse implements ResponseInterface $this->remaining = -2; } - if ($this->inflate && 'gzip' !== ($this->headers['content-encoding'][0] ?? null)) { - $this->inflate = null; - } - - try { - if (null !== $this->shouldBuffer && null === $this->content && $this->content = ($this->shouldBuffer)($this->headers) ?: null) { - $this->content = \is_resource($this->content) ? $this->content : fopen('php://temp', 'w+'); - } - - if (null !== $this->info['error']) { - throw new TransportException($this->info['error']); - } - } catch (\Throwable $e) { - $this->close(); - $this->multi->handlesActivity[$this->id] = [new FirstChunk()]; - $this->multi->handlesActivity[$this->id][] = null; - $this->multi->handlesActivity[$this->id][] = $e; - - return; - } - $this->multi->handlesActivity[$this->id] = [new FirstChunk()]; if ('HEAD' === $context['http']['method'] || \in_array($this->info['http_code'], [204, 304], true)) { @@ -209,7 +176,7 @@ final class NativeResponse implements ResponseInterface return; } - $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->inflate, $this->content, $this->onProgress, &$this->remaining, &$this->info]; + $this->multi->openHandles[$this->id] = [$h, $this->buffer, $this->onProgress, &$this->remaining, &$this->info]; } /** @@ -249,15 +216,15 @@ final class NativeResponse implements ResponseInterface $multi->handles = []; } - foreach ($multi->openHandles as $i => [$h, $buffer, $inflate, $content, $onProgress]) { + foreach ($multi->openHandles as $i => [$h, $buffer, $onProgress]) { $hasActivity = false; - $remaining = &$multi->openHandles[$i][5]; - $info = &$multi->openHandles[$i][6]; + $remaining = &$multi->openHandles[$i][3]; + $info = &$multi->openHandles[$i][4]; $e = null; // Read incoming buffer and write it to the dechunk one try { - while ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) { + if ($remaining && '' !== $data = (string) fread($h, 0 > $remaining ? 16372 : $remaining)) { fwrite($buffer, $data); $hasActivity = true; $multi->sleep = false; @@ -285,16 +252,8 @@ final class NativeResponse implements ResponseInterface rewind($buffer); ftruncate($buffer, 0); - if (null !== $inflate && false === $data = @inflate_add($inflate, $data)) { - $e = new TransportException('Error while processing content unencoding.'); - } - - if ('' !== $data && null === $e) { + if (null === $e) { $multi->handlesActivity[$i][] = $data; - - if (null !== $content && \strlen($data) !== fwrite($content, $data)) { - $e = new TransportException(sprintf('Failed writing %d bytes to the response buffer.', \strlen($data))); - } } } diff --git a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php index 71a04f1e8a..1222a4b1e9 100644 --- a/src/Symfony/Component/HttpClient/Response/ResponseTrait.php +++ b/src/Symfony/Component/HttpClient/Response/ResponseTrait.php @@ -43,11 +43,6 @@ trait ResponseTrait */ private $initializer; - /** - * @var resource A php://temp stream typically - */ - private $content; - private $info = [ 'response_headers' => [], 'http_code' => 0, @@ -59,6 +54,9 @@ trait ResponseTrait private $handle; private $id; private $timeout = 0; + private $inflate; + private $shouldBuffer; + private $content; private $finalInfo; private $offset = 0; private $jsonData; @@ -69,8 +67,7 @@ trait ResponseTrait public function getStatusCode(): int { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } return $this->info['http_code']; @@ -82,8 +79,7 @@ trait ResponseTrait public function getHeaders(bool $throw = true): array { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } if ($throw) { @@ -99,8 +95,7 @@ trait ResponseTrait public function getContent(bool $throw = true): string { if ($this->initializer) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); } if ($throw) { @@ -231,6 +226,30 @@ trait ResponseTrait */ abstract protected static function select(ClientState $multi, float $timeout): int; + private static function initialize(self $response): void + { + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + + try { + if (($response->initializer)($response)) { + foreach (self::stream([$response]) as $chunk) { + if ($chunk->isFirst()) { + break; + } + } + } + } catch (\Throwable $e) { + // Persist timeouts thrown during initialization + $response->info['error'] = $e->getMessage(); + $response->close(); + throw $e; + } + + $response->initializer = null; + } + private static function addResponseHeaders(array $responseHeaders, array &$info, array &$headers, string &$debug = ''): void { foreach ($responseHeaders as $h) { @@ -276,8 +295,7 @@ trait ResponseTrait private function doDestruct() { if ($this->initializer && null === $this->info['error']) { - ($this->initializer)($this); - $this->initializer = null; + self::initialize($this); $this->checkStatusCode(); } } @@ -329,6 +347,16 @@ trait ResponseTrait $isTimeout = false; if (\is_string($chunk = array_shift($multi->handlesActivity[$j]))) { + if (null !== $response->inflate && false === $chunk = @inflate_add($response->inflate, $chunk)) { + $multi->handlesActivity[$j] = [null, new TransportException('Error while processing content unencoding.')]; + continue; + } + + if ('' !== $chunk && null !== $response->content && \strlen($chunk) !== fwrite($response->content, $chunk)) { + $multi->handlesActivity[$j] = [null, new TransportException('Failed writing %d bytes to the response buffer.', \strlen($chunk))]; + continue; + } + $response->offset += \strlen($chunk); $chunk = new DataChunk($response->offset, $chunk); } elseif (null === $chunk) { @@ -356,6 +384,28 @@ trait ResponseTrait $response->logger->info(sprintf('Response: "%s %s"', $info['http_code'], $info['url'])); } + $response->inflate = \extension_loaded('zlib') && $response->inflate && 'gzip' === ($response->headers['content-encoding'][0] ?? null) ? inflate_init(ZLIB_ENCODING_GZIP) : null; + + if ($response->shouldBuffer instanceof \Closure) { + try { + $response->shouldBuffer = ($response->shouldBuffer)($response->headers); + + if (null !== $response->info['error']) { + throw new TransportException($response->info['error']); + } + } catch (\Throwable $e) { + $response->close(); + $multi->handlesActivity[$j] = [null, $e]; + } + } + + if (true === $response->shouldBuffer) { + $response->content = fopen('php://temp', 'w+'); + } elseif (\is_resource($response->shouldBuffer)) { + $response->content = $response->shouldBuffer; + } + $response->shouldBuffer = null; + yield $response => $chunk; if ($response->initializer && null === $response->info['error']) {