8000 Merge branch '7.4' into 8.0 · symfony/symfony@8f8147b · GitHub
[go: up one dir, main page]

Skip to content

Commit 8f8147b

Browse files
Merge branch '7.4' into 8.0
* 7.4: [DependencyInjection] Fix lazy proxy type resolution for decorated services [HttpClient] Fix dealing with truncated streams after headers arrived with CurlHttpClient [PropertyInfo] Fix DocBlock resolution for inherited promoted properties [HttpFoundation] Fix PdoSessionHandler charset-collation mismatch with the Doctrine DBAL on MySQL [HttpClient] Fix dealing with multiple levels of AsyncResponse decoration [Messenger] Only send UNLISTEN query if we are actively listening [RateLimiter] Persist state when consuming negative tokens
2 parents 76f665a + 6885ec9 commit 8f8147b

File tree

20 files changed

+405
-24
lines changed

20 files changed

+405
-24
lines changed

src/Symfony/Component/Cache/Adapter/PdoAdapter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ public function createTable(): void
118118
// - trailing space removal
119119
// - case-insensitivity
120120
// - language processing like é == e
121-
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
121+
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(255) NOT NULL PRIMARY KEY, $this->dataCol MEDIUMBLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED, $this->timeCol INTEGER UNSIGNED NOT NULL), ENGINE = InnoDB",
122122
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
123123
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(255) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",
124124
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(255) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER, $this->timeCol INTEGER NOT NULL)",

src/Symfony/Component/DependencyInjection/Compiler/AutowirePass.php

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -335,11 +335,6 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
335335
$value = $this->doProcessValue($value);
336336
} elseif ($lazy = $attribute->lazy) {
337337
$value ??= $getValue();
338-
$type = $this->resolveProxyType($type, $value->getType());
339-
$definition = (new Definition($type))
340-
->setFactory('current')
341-
->setArguments([[$value]])
342-
->setLazy(true);
343338

344339
if (!\is_array($lazy)) {
345340
if (str_contains($type, '|')) {
@@ -348,8 +343,14 @@ private function autowireMethod(\ReflectionFunctionAbstract $reflectionMethod, a
348343
$lazy = str_contains($type, '&') ? explode('&', $type) : [];
349344
}
350345

346+
$proxyType = $lazy ? $type : $this->resolveProxyType($type, $value);
347+
$definition = (new Definition($proxyType))
348+
->setFactory('current')
349+
->setArguments([[$value]])
350+
->setLazy(true);
351+
351352
if ($lazy) {
352-
if (!class_exists($type) && !interface_exists($type, false)) {
353+
if (!$this->container->getReflectionClass($proxyType, false)) {
353354
$definition->setClass('object');
354355
}
355356
foreach ($lazy as $v) {
@@ -757,7 +758,7 @@ private function resolveProxyType(string $originalType, string $serviceId): stri
757758
$resolvedType = $this->container->findDefinition($serviceId)->getClass();
758759
$resolvedType = $this->container->getParameterBag()->resolveValue($resolvedType);
759760

760-
if (!$resolvedType || !class_exists($resolvedType, false) && !interface_exists($resolvedType, false)) {
761+
if (!$resolvedType || !$this->container->getReflectionClass($resolvedType, false)) {
761762
return $originalType;
762763
}
763764

src/Symfony/Component/DependencyInjection/Tests/Compiler/AutowirePassTest.php

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,4 +1487,30 @@ public function testLazyProxyWithClassInheritance()
14871487
$dep = $service->getDependency()->getSelf();
14881488
$this->assertInstanceOf(ExtendedLazyProxyClass::class, $dep);
14891489
}
1490+
1491+
public function testLazyProxyForDecoratedService()
1492+
{
1493+
$container = new ContainerBuilder();
1494+
1495+
$container->register('some_service', SomeServiceClass::class)
1496+
->setPublic(true);
1497+
1498+
$container->register('some_service.decorated', DecoratedSomeServiceClass::class)
1499+
->setDecoratedService('some_service')
1500+
->addArgument(new Reference('.inner'));
1501+
1502+
$container->register(LazyDecoratedServiceConsumer::class)
1503+
->setAutowired(true)
1504+
->setPublic(true);
1505+
1506+
$container->setAlias(SomeServiceInterface::class, 'some_service');
1507+
1508+
$container->compile();
1509+
1510+
$service = $container->get(LazyDecoratedServiceConsumer::class);
1511+
$this->assertInstanceOf(LazyDecoratedServiceConsumer::class, $service);
1512+
1513+
$result = $service->getValue();
1514+
$this->assertSame('decorated:original', $result);
1515+
}
14901516
}

src/Symfony/Component/DependencyInjection/Tests/Fixtures/includes/autowiring_classes.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -605,3 +605,42 @@ public function __construct(public ContainerInterface $container)
605605
{
606606
}
607607
}
608+
609+
interface SomeServiceInterface
610+
{
611+
public function getValue(): string;
612+
}
613+
614+
class SomeServiceClass implements SomeServiceInterface
615+
{
616+
public function getValue(): string
617+
{
618+
return 'original';
619+
}
620+
}
621+
622+
class DecoratedSomeServiceClass implements SomeServiceInterface
623+
{
624+
public function __construct(private SomeServiceInterface $inner)
625+
{
626+
}
627+
628+
public function getValue(): string
629+
{
630+
return 'decorated:'.$this->inner->getValue();
631+
}
632+
}
633+
634+
class LazyDecoratedServiceConsumer
635+
{
636+
public function __construct(
637+
#[Autowire(service: 'some_service', lazy: true)]
638+
private SomeServiceInterface $service,
639+
) {
640+
}
641+
642+
public function getValue(): string
643+
{
644+
return $this->service->getValue();
645+
}
646+
}

src/Symfony/Component/HttpClient/Response/AsyncResponse.php

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public function __construct(HttpClientInterface $client, string $method, string
6464

6565
while (true) {
6666
foreach (self::stream([$response], $timeout) as $chunk) {
67-
if ($chunk->isTimeout() && $response->passthru) {
67+
if ($chunk->isTimeout() && ($response->passthru || $response = self::findInnerPassthru($response))) {
6868
// Timeouts thrown during initialization are transport errors
6969
foreach (self::passthru($response->client, $response, new ErrorChunk($response->offset, new TransportException($chunk->getError()))) as $chunk) {
7070
if ($chunk->isFirst()) {
@@ -274,7 +274,8 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri
274274
}
275275
}
276276

277-
if (!$r->passthru) {
277+
$innerR = null;
278+
if (!$r->passthru && !$innerR = null !== $chunk->getError() ? self::findInnerPassthru($r) : null) {
278279
$r->stream = (static fn () => yield $chunk)();
279280
yield from self::passthruStream($response, $r, $asyncMap);
280281

@@ -289,11 +290,12 @@ public static function stream(iterable $responses, ?float $timeout = null, ?stri
289290
throw new \LogicException(\sprintf('Instance of "%s" is already consumed and cannot be managed by "%s". A decorated client should not call any of the response\'s methods in its "request()" method.', get_debug_type($response), $class ?? static::class));
290291
}
291292

292-
foreach (self::passthru($r->client, $r, $chunk, $asyncMap) as $chunk) {
293+
$innerR ??= $r;
294+
foreach (self::passthru($innerR->client, $innerR, $chunk, $asyncMap) as $chunk) {
293295
yield $r => $chunk;
294296
}
295297

296-
if ($r->response !== $response && isset($asyncMap[$response])) {
298+
if ($innerR->response !== $response && isset($asyncMap[$response])) {
297299
break;
298300
}
299301
}
@@ -343,6 +345,21 @@ private static function passthru(HttpClientInterface $client, self $r, ChunkInte
343345
yield from self::passthruStream($response, $r, $asyncMap);
344346
}
345347

348+
private static function findInnerPassthru(self $response): ?self
349+
{
350+
$innerResponse = $response->response ?? null;
351+
352+
while ($innerResponse instanceof self) {
353+
if ($innerResponse->passthru) {
354+
return $innerResponse;
355+
}
356+
357+
$innerResponse = $innerResponse->response ?? null;
358+
}
359+
360+
return null;
361+
}
362+
346363
/**
347364
* @param \SplObjectStorage<ResponseInterface, AsyncResponse>|null $asyncMap
348365
*/

src/Symfony/Component/HttpClient/Response/CurlResponse.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,6 +321,7 @@ private static function perform(ClientState $multi, ?array $responses = null): v
321321

322322
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
323323
$multi->handlesActivity[$id][] = new FirstChunk();
324+
curl_setopt($ch, \CURLOPT_PRIVATE, 'C'.$waitFor[1]);
324325
}
325326

326327
$multi->handlesActivity[$id][] = null;

src/Symfony/Component/HttpClient/Tests/RetryableHttpClientTest.php

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,14 @@
1818
use Symfony\Component\HttpClient\MockHttpClient;
1919
use Symfony\Component\HttpClient\NativeHttpClient;
2020
use Symfony\Component\HttpClient\Response\AsyncContext;
21+
use Symfony\Component\HttpClient\Response\AsyncResponse;
2122
use Symfony\Component\HttpClient\Response\MockResponse;
2223
use Symfony\Component\HttpClient\Retry\GenericRetryStrategy;
2324
use Symfony\Component\HttpClient\Retry\RetryStrategyInterface;
2425
use Symfony\Component\HttpClient\RetryableHttpClient;
2526
use Symfony\Contracts\HttpClient\Exception\< BF21 span class=pl-smi>TransportExceptionInterface;
27+
use Symfony\Contracts\HttpClient\HttpClientInterface;
28+
use Symfony\Contracts\HttpClient\ResponseInterface;
2629
use Symfony\Contracts\HttpClient\Test\TestHttpServer;
2730

2831
class RetryableHttpClientTest extends TestCase
@@ -421,4 +424,174 @@ public function testMaxRetriesWithOptions()
421424

422425
self::assertSame(504, $response->getStatusCode());
423426
}
427+
428+
public function testRetryOnTimeoutWithAsyncDecorator()
429+
{
430+
$client = HttpClient::create();
431+
432+
TestHttpServer::start();
433+
434+
$strategy = new class implements RetryStrategyInterface {
435+
public bool $isCalled = false;
436+
437+
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
438+
{
439+
$this->isCalled = true;
440+
441+
return false;
442+
}
443+
444+
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
445+
{
446+
return 0;
447+
}
448+
};
449+
$client = new RetryableHttpClient($client, $strategy);
450+
451+
$client = new class($client) implements HttpClientInterface {
452+
use \Symfony\Component\HttpClient\AsyncDecoratorTrait;
453+
454+
public function request(string $method, string $url, array $options = []): ResponseInterface
455+
{
456+
return new AsyncResponse($this->client, $method, $url, $options);
457+
}
458+
};
459+
460+
$response = $client->request('GET', 'http://localhost:8057/timeout-header', ['timeout' => 0.1]);
461+
462+
try {
463+
$response->getStatusCode();
464+
$this->fail(TransportException::class.' expected');
465+
} catch (TransportException $e) {
466+
}
467+
468+
$this->assertTrue($strategy->isCalled, 'The HTTP retry strategy should be called');
469+
}
470+
471+
public function testRetryOnErrorWithAsyncDecorator()
472+
{
473+
$client = new MockHttpClient([
474+
new MockResponse('', ['http_code' => 500]),
475+
new MockResponse('OK', ['http_code' => 200]),
476+
]);
477+
478+
$client = new RetryableHttpClient($client, new GenericRetryStrategy([500], 0), 1);
479+
480+
$client = new class($client) implements HttpClientInterface {
481+
use \Symfony\Component\HttpClient\AsyncDecoratorTrait;
482+
483+
public function request(string $method, string $url, array $options = []): ResponseInterface
484+
{
485+
return new AsyncResponse($this->client, $method, $url, $options);
486+
}
487+
};
488+
489+
$response = $client->request('GET', 'http://example.com/foo-bar');
490+
491+
self::assertSame(200, $response->getStatusCode());
492+
self::assertSame('OK', $response->getContent());
493+
}
494+
495+
public function testRetryOnTimeoutWithMultipleAsyncDecorators()
496+
{
497+
$client = HttpClient::create();
498+
499+
TestHttpServer::start();
500+
501+
$strategy = new class implements RetryStrategyInterface {
502+
public bool $isCalled = false;
503+
504+
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
505+
{
506+
$this->isCalled = true;
507+
508+
return false;
509+
}
510+
511+
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
512+
{
513+
return 0;
514+
}
515+
};
516+
517+
$client = new RetryableHttpClient($client, $strategy);
518+
519+
// Two nested async decorators without passthru around the RetryableHttpClient
520+
$client = new class($client) implements HttpClientInterface {
521+
use \Symfony\Component\HttpClient\AsyncDecoratorTrait;
522+
523+
public function request(string $method, string $url, array $options = []): ResponseInterface
524+
{
525+
return new AsyncResponse($this->client, $method, $url, $options);
526+
}
527+
};
528+
$client = new class($client) implements HttpClientInterface {
529+
use \Symfony\Component\HttpClient\AsyncDecoratorTrait;
530+
531+
public function request(string $method, string $url, array $options = []): ResponseInterface
532+
{
533+
return new AsyncResponse($this->client, $method, $url, $options);
534+
}
535+
};
536+
537+
$response = $client->request('GET', 'http://localhost:8057/timeout-header', ['timeout' => 0.1]);
538+
539+
try {
540+
$response->getStatusCode();
541+
$this->fail(TransportException::class.' expected');
542+
} catch (TransportException $e) {
543+
}
544+
545+
$this->assertTrue($strategy->isCalled, 'The HTTP retry strategy should be called with multiple decorators');
546+
}
547+
548+
public function testRetryActuallyRetriesWithAsyncDecorator()
549+
{
550+
$client = HttpClient::create();
551+
552+
TestHttpServer::start();
553+
554+
$retryCount = 0;
555+
$strategy = new class($retryCount) implements RetryStrategyInterface {
556+
private int $retryCount;
557+
558+
public function __construct(int &$retryCount)
559+
{
560+
$this->retryCount = &$retryCount;
561+
}
562+
563+
public function shouldRetry(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): ?bool
564+
{
565+
++$this->retryCount;
566+
567+
return $this->retryCount < 2;
568+
}
569+
570+
public function getDelay(AsyncContext $context, ?string $responseContent, ?TransportExceptionInterface $exception): int
571+
{
572+
return 0;
573+
}
574+
};
575+
576+
$client = new RetryableHttpClient($client, $strategy, 2);
577+
578+
$client = new class($client) implements HttpClientInterface {
579+
use \Symfony\Component\HttpClient\AsyncDecoratorTrait;
580+
581+
public function request(string $method, string $url, array $options = []): ResponseInterface
582+
{
583+
return new AsyncResponse($this->client, $method, $url, $options);
584+
}
585+
};
586+
587+
$response = $client->request('GET', 'http://localhost:8057/timeout-header', ['timeout' => 0.1]);
588+
589+
try {
590+
$response->getStatusCode();
591+
$this->fail(TransportException::class.' expected');
592+
} catch (TransportException $e) {
593+
}
594+
595+
$this->assertSame(2, $retryCount, 'The request should have been retried once');
596+
}
424597
}

src/Symfony/Component/HttpFoundation/Session/Storage/Handler/PdoSessionHandler.php

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ public function configureSchema(Schema $schema, ?\Closure $isSameDatabase = null
197197
$table->addColumn($this->dataCol, Types::BLOB)->setNotnull(true);
198198
$table->addColumn($this->lifetimeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
199199
$table->addColumn($this->timeCol, Types::INTEGER)->setUnsigned(true)->setNotnull(true);
200-
$table->addOption('collate', 'utf8mb4_bin');
201200
$table->addOption('engine', 'InnoDB');
202201
break;
203202
case 'sqlite':
@@ -254,7 +253,7 @@ public function createTable(): void
254253
// - trailing space removal
255254
// - case-insensitivity
256255
// - language processing like é == e
257-
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
256+
'mysql' => "CREATE TABLE $this->table ($this->idCol VARBINARY(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER UNSIGNED NOT NULL, $this->timeCol INTEGER UNSIGNED NOT NULL), ENGINE = InnoDB",
258257
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
259258
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(128) NOT NULL PRIMARY KEY, $this->dataCol BYTEA NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",
260259
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(128) NOT NULL PRIMARY KEY, $this->dataCol BLOB NOT NULL, $this->lifetimeCol INTEGER NOT NULL, $this->timeCol INTEGER NOT NULL)",

src/Symfony/Component/Lock/Store/PdoStore.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ private function getConnection(): \PDO
188188
public function createTable(): void
189189
{
190190
$sql = match ($driver = $this->getDriver()) {
191-
'mysql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL) COLLATE utf8mb4_bin, ENGINE = InnoDB",
191+
'mysql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(44) NOT NULL, $this->expirationCol INTEGER UNSIGNED NOT NULL), ENGINE = InnoDB",
192192
'sqlite' => "CREATE TABLE $this->table ($this->idCol TEXT NOT NULL PRIMARY KEY, $this->tokenCol TEXT NOT NULL, $this->expirationCol INTEGER)",
193193
'pgsql' => "CREATE TABLE $this->table ($this->idCol VARCHAR(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR(64) NOT NULL, $this->expirationCol INTEGER)",
194194
'oci' => "CREATE TABLE $this->table ($this->idCol VARCHAR2(64) NOT NULL PRIMARY KEY, $this->tokenCol VARCHAR2(64) NOT NULL, $this->expirationCol INTEGER)",

0 commit comments

Comments
 (0)
0