Merge branch '5.4' into 6.0

* 5.4:
  [HttpClient] fix resetting DNS/etc when calling CurlHttpClient::reset()
  Fix invalid guess with enumType
  [HttpClient] Remove deprecated usage of GuzzleHttp\Promise\promise_for
This commit is contained in:
Nicolas Grekas 2022-01-17 11:54:40 +01:00
commit adc8a55bad
13 changed files with 216 additions and 84 deletions

View File

@ -135,14 +135,17 @@ class DoctrineExtractor implements PropertyListExtractorInterface, PropertyTypeE
}
if ($metadata->hasField($property)) {
$nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property);
if (null !== $enumClass = $metadata->getFieldMapping($property)['enumType'] ?? null) {
return [new Type(Type::BUILTIN_TYPE_OBJECT, $nullable, $enumClass)];
}
$typeOfField = $metadata->getTypeOfField($property);
if (!$builtinType = $this->getPhpType($typeOfField)) {
return null;
}
$nullable = $metadata instanceof ClassMetadataInfo && $metadata->isNullable($property);
switch ($builtinType) {
case Type::BUILTIN_TYPE_OBJECT:
switch ($typeOfField) {

View File

@ -0,0 +1,36 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\Fixtures;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
*/
class DoctrineLoaderEnum
{
/**
* @ORM\Id
* @ORM\Column
*/
public $id;
/**
* @ORM\Column(type="string", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString", length=1)
*/
public $enumString;
/**
* @ORM\Column(type="integer", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt")
*/
public $enumInt;
}

View File

@ -14,12 +14,16 @@ namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Tools\Setup;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\PropertyInfo\DoctrineExtractor;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineDummy;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineEnum;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineGeneratedValue;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\DoctrineRelation;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt;
use Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString;
use Symfony\Component\PropertyInfo\Type;
/**
@ -124,6 +128,18 @@ class DoctrineExtractorTest extends TestCase
$this->assertEquals($expectedTypes, $actualTypes);
}
/**
* @requires PHP 8.1
*/
public function testExtractEnum()
{
if (!property_exists(Column::class, 'enumType')) {
$this->markTestSkipped('The "enumType" requires doctrine/orm 2.11.');
}
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumString::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumString', []));
$this->assertEquals([new Type(Type::BUILTIN_TYPE_OBJECT, false, EnumInt::class)], $this->createExtractor()->getTypes(DoctrineEnum::class, 'enumInt', []));
}
public function typesProvider()
{
$provider = [

View File

@ -0,0 +1,38 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
/**
* @Entity
*/
class DoctrineEnum
{
/**
* @Id
* @Column(type="smallint")
*/
public $id;
/**
* @Column(type="string", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumString")
*/
protected $enumString;
/**
* @Column(type="integer", enumType="Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures\EnumInt")
*/
protected $enumInt;
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures;
enum EnumInt: int
{
case Foo = 0;
case Bar = 1;
}

View File

@ -0,0 +1,18 @@
<?php
/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bridge\Doctrine\Tests\PropertyInfo\Fixtures;
enum EnumString: string
{
case Foo = 'f';
case Bar = 'b';
}

View File

@ -11,11 +11,13 @@
namespace Symfony\Bridge\Doctrine\Tests\Validator;
use Doctrine\ORM\Mapping\Column;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Tests\DoctrineTestHelper;
use Symfony\Bridge\Doctrine\Tests\Fixtures\BaseUser;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEmbed;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderEnum;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNestedEmbed;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderNoAutoMappingEntity;
use Symfony\Bridge\Doctrine\Tests\Fixtures\DoctrineLoaderParentEntity;
@ -149,6 +151,31 @@ class DoctrineLoaderTest extends TestCase
$this->assertSame(AutoMappingStrategy::DISABLED, $noAutoMappingMetadata[0]->getAutoMappingStrategy());
}
/**
* @requires PHP 8.1
*/
public function testExtractEnum()
{
if (!property_exists(Column::class, 'enumType')) {
$this->markTestSkipped('The "enumType" requires doctrine/orm 2.11.');
}
$validator = Validation::createValidatorBuilder()
->addMethodMapping('loadValidatorMetadata')
->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}'))
->getValidator()
;
$classMetadata = $validator->getMetadataFor(new DoctrineLoaderEnum());
$enumStringMetadata = $classMetadata->getPropertyMetadata('enumString');
$this->assertCount(0, $enumStringMetadata); // asserts the length constraint is not added to an enum
$enumStringMetadata = $classMetadata->getPropertyMetadata('enumInt');
$this->assertCount(0, $enumStringMetadata); // asserts the length constraint is not added to an enum
}
public function testFieldMappingsConfiguration()
{
$validator = Validation::createValidatorBuilder()

View File

@ -16,6 +16,7 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
use Doctrine\Persistence\Mapping\MappingException;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\PropertyInfo\Type;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Mapping\AutoMappingStrategy;
@ -99,7 +100,7 @@ final class DoctrineLoader implements LoaderInterface
$loaded = true;
}
if (null === ($mapping['length'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
if (null === ($mapping['length'] ?? null) || null !== ($mapping['enumType'] ?? null) || !\in_array($mapping['type'], ['string', 'text'], true)) {
continue;
}

View File

@ -166,7 +166,6 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
if ($resolve && 0x072A00 > CurlClientState::$curlVersion['version_number']) {
// DNS cache removals require curl 7.42 or higher
// On lower versions, we have to create a new multi handle
$this->multi->reset();
}
@ -283,6 +282,7 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
if (!$pushedResponse) {
$ch = curl_init();
$this->logger && $this->logger->info(sprintf('Request: "%s %s"', $method, $url));
$curlopts += [\CURLOPT_SHARE => $this->multi->share];
}
foreach ($curlopts as $opt => $value) {
@ -304,9 +304,9 @@ final class CurlHttpClient implements HttpClientInterface, LoggerAwareInterface,
$responses = [$responses];
}
if (($mh = $this->multi->handles[0] ?? null) instanceof \CurlMultiHandle) {
if ($this->multi->handle instanceof \CurlMultiHandle) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($mh, $active)) {
while (\CURLM_CALL_MULTI_PERFORM === curl_multi_exec($this->multi->handle, $active)) {
}
}

View File

@ -23,11 +23,11 @@ use Symfony\Component\HttpClient\Response\CurlResponse;
*/
final class CurlClientState extends ClientState
{
/** @var array<\CurlMultiHandle> */
public array $handles = [];
public \CurlMultiHandle $handle;
public \CurlShareHandle $share;
/** @var PushedResponse[] */
public array $pushedResponses = [];
public $dnsCache;
public DnsCache $dnsCache;
/** @var float[] */
public array $pauseExpiries = [];
public int $execCounter = \PHP_INT_MIN;
@ -35,27 +35,23 @@ final class CurlClientState extends ClientState
public static array $curlVersion;
private int $maxHostConnections;
private int $maxPendingPushes;
public function __construct(int $maxHostConnections, int $maxPendingPushes)
{
self::$curlVersion = self::$curlVersion ?? curl_version();
array_unshift($this->handles, $mh = curl_multi_init());
$this->handle = curl_multi_init();
$this->dnsCache = new DnsCache();
$this->maxHostConnections = $maxHostConnections;
$this->maxPendingPushes = $maxPendingPushes;
$this->reset();
// Don't enable HTTP/1.1 pipelining: it forces responses to be sent in order
if (\defined('CURLPIPE_MULTIPLEX')) {
curl_multi_setopt($mh, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
curl_multi_setopt($this->handle, \CURLMOPT_PIPELINING, \CURLPIPE_MULTIPLEX);
}
if (\defined('CURLMOPT_MAX_HOST_CONNECTIONS')) {
$maxHostConnections = curl_multi_setopt($mh, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
$maxHostConnections = curl_multi_setopt($this->handle, \CURLMOPT_MAX_HOST_CONNECTIONS, 0 < $maxHostConnections ? $maxHostConnections : \PHP_INT_MAX) ? 0 : $maxHostConnections;
}
if (\defined('CURLMOPT_MAXCONNECTS') && 0 < $maxHostConnections) {
curl_multi_setopt($mh, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
curl_multi_setopt($this->handle, \CURLMOPT_MAXCONNECTS, $maxHostConnections);
}
// Skip configuring HTTP/2 push when it's unsupported or buggy, see https://bugs.php.net/77535
@ -68,17 +64,8 @@ final class CurlClientState extends ClientState
return;
}
// Clone to prevent a circular reference
$multi = clone $this;
$multi->handles = [$mh];
$multi->pushedResponses = &$this->pushedResponses;
$multi->logger = &$this->logger;
$multi->handlesActivity = &$this->handlesActivity;
$multi->openHandles = &$this->openHandles;
$multi->lastTimeout = &$this->lastTimeout;
curl_multi_setopt($mh, \CURLMOPT_PUSHFUNCTION, static function ($parent, $pushed, array $requestHeaders) use ($multi, $maxPendingPushes) {
return $multi->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
curl_multi_setopt($this->handle, \CURLMOPT_PUSHFUNCTION, function ($parent, $pushed, array $requestHeaders) use ($maxPendingPushes) {
return $this->handlePush($parent, $pushed, $requestHeaders, $maxPendingPushes);
});
}
@ -86,10 +73,7 @@ final class CurlClientState extends ClientState
{
foreach ($this->pushedResponses as $url => $response) {
$this->logger && $this->logger->debug(sprintf('Unused pushed response: "%s"', $url));
foreach ($this->handles as $mh) {
curl_multi_remove_handle($mh, $response->handle);
}
curl_multi_remove_handle($this->handle, $response->handle);
curl_close($response->handle);
}
@ -97,11 +81,14 @@ final class CurlClientState extends ClientState
$this->dnsCache->evictions = $this->dnsCache->evictions ?: $this->dnsCache->removals;
$this->dnsCache->removals = $this->dnsCache->hostnames = [];
if (\defined('CURLMOPT_PUSHFUNCTION')) {
curl_multi_setopt($this->handles[0], \CURLMOPT_PUSHFUNCTION, null);
}
$this->share = curl_share_init();
$this->__construct($this->maxHostConnections, $this->maxPendingPushes);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_DNS);
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_SSL_SESSION);
if (\defined('CURL_LOCK_DATA_CONNECT')) {
curl_share_setopt($this->share, \CURLSHOPT_SHARE, \CURL_LOCK_DATA_CONNECT);
}
}
private function handlePush($parent, $pushed, array $requestHeaders, int $maxPendingPushes): int

View File

@ -108,9 +108,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
if (0 < $duration) {
if ($execCounter === $multi->execCounter) {
$multi->execCounter = !\is_float($execCounter) ? 1 + $execCounter : \PHP_INT_MIN;
foreach ($multi->handles as $mh) {
curl_multi_remove_handle($mh, $ch);
}
curl_multi_remove_handle($multi->handle, $ch);
}
$lastExpiry = end($multi->pauseExpiries);
@ -122,7 +120,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
} else {
unset($multi->pauseExpiries[(int) $ch]);
curl_pause($ch, \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handles[0], $ch);
curl_multi_add_handle($multi->handle, $ch);
}
};
@ -176,7 +174,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
// Schedule the request in a non-blocking way
$multi->lastTimeout = null;
$multi->openHandles[$id] = [$ch, $options];
curl_multi_add_handle($multi->handles[0], $ch);
curl_multi_add_handle($multi->handle, $ch);
$this->canary = new Canary(static function () use ($ch, $multi, $id) {
unset($multi->pauseExpiries[$id], $multi->openHandles[$id], $multi->handlesActivity[$id]);
@ -186,9 +184,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
return;
}
foreach ($multi->handles as $mh) {
curl_multi_remove_handle($mh, $ch);
}
curl_multi_remove_handle($multi->handle, $ch);
curl_setopt_array($ch, [
\CURLOPT_NOPROGRESS => true,
\CURLOPT_PROGRESSFUNCTION => null,
@ -270,7 +266,7 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
*/
private static function schedule(self $response, array &$runningResponses): void
{
if (isset($runningResponses[$i = (int) $response->multi->handles[0]])) {
if (isset($runningResponses[$i = (int) $response->multi->handle])) {
$runningResponses[$i][1][$response->id] = $response;
} else {
$runningResponses[$i] = [$response->multi, [$response->id => $response]];
@ -303,47 +299,39 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
try {
self::$performing = true;
++$multi->execCounter;
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($multi->handle, $active))) {
}
foreach ($multi->handles as $i => $mh) {
$active = 0;
while (\CURLM_CALL_MULTI_PERFORM === ($err = curl_multi_exec($mh, $active))) {
if (\CURLM_OK !== $err) {
throw new TransportException(curl_multi_strerror($err));
}
while ($info = curl_multi_info_read($multi->handle)) {
if (\CURLMSG_DONE !== $info['msg']) {
continue;
}
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\CURLM_OK !== $err) {
throw new TransportException(curl_multi_strerror($err));
}
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($multi->handle, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
while ($info = curl_multi_info_read($mh)) {
if (\CURLMSG_DONE !== $info['msg']) {
if (0 === curl_multi_add_handle($multi->handle, $ch)) {
continue;
}
$result = $info['result'];
$id = (int) $ch = $info['handle'];
$waitFor = @curl_getinfo($ch, \CURLINFO_PRIVATE) ?: '_0';
if (\in_array($result, [\CURLE_SEND_ERROR, \CURLE_RECV_ERROR, /*CURLE_HTTP2*/ 16, /*CURLE_HTTP2_STREAM*/ 92], true) && $waitFor[1] && 'C' !== $waitFor[0]) {
curl_multi_remove_handle($mh, $ch);
$waitFor[1] = (string) ((int) $waitFor[1] - 1); // decrement the retry counter
curl_setopt($ch, \CURLOPT_PRIVATE, $waitFor);
curl_setopt($ch, \CURLOPT_FORBID_REUSE, true);
if (0 === curl_multi_add_handle($mh, $ch)) {
continue;
}
}
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
if (!$active && 0 < $i) {
curl_multi_close($mh);
unset($multi->handles[$i]);
if (\CURLE_RECV_ERROR === $result && 'H' === $waitFor[0] && 400 <= ($responses[(int) $ch]->info['http_code'] ?? 0)) {
$multi->handlesActivity[$id][] = new FirstChunk();
}
$multi->handlesActivity[$id][] = null;
$multi->handlesActivity[$id][] = \in_array($result, [\CURLE_OK, \CURLE_TOO_MANY_REDIRECTS], true) || '_0' === $waitFor || curl_getinfo($ch, \CURLINFO_SIZE_DOWNLOAD) === curl_getinfo($ch, \CURLINFO_CONTENT_LENGTH_DOWNLOAD) ? null : new TransportException(ucfirst(curl_error($ch) ?: curl_strerror($result)).sprintf(' for "%s".', curl_getinfo($ch, \CURLINFO_EFFECTIVE_URL)));
}
} finally {
self::$performing = false;
@ -368,11 +356,11 @@ final class CurlResponse implements ResponseInterface, StreamableInterface
unset($multi->pauseExpiries[$id]);
curl_pause($multi->openHandles[$id][0], \CURLPAUSE_CONT);
curl_multi_add_handle($multi->handles[0], $multi->openHandles[$id][0]);
curl_multi_add_handle($multi->handle, $multi->openHandles[$id][0]);
}
}
if (0 !== $selected = curl_multi_select($multi->handles[array_key_last($multi->handles)], $timeout)) {
if (0 !== $selected = curl_multi_select($multi->handle, $timeout)) {
return $selected;
}

View File

@ -11,7 +11,7 @@
namespace Symfony\Component\HttpClient\Response;
use function GuzzleHttp\Promise\promise_for;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface as GuzzlePromiseInterface;
use Http\Promise\Promise as HttplugPromiseInterface;
use Psr\Http\Message\ResponseInterface as Psr7ResponseInterface;
@ -74,7 +74,7 @@ final class HttplugPromise implements HttplugPromiseInterface
}
return static function ($value) use ($callback) {
return promise_for($callback($value));
return Create::promiseFor($callback($value));
};
}
}

View File

@ -62,9 +62,9 @@ class CurlHttpClientTest extends HttpClientTestCase
$r = new \ReflectionProperty($httpClient, 'multi');
$r->setAccessible(true);
$clientState = $r->getValue($httpClient);
$initialHandleId = (int) $clientState->handles[0];
$initialShareId = $clientState->share;
$httpClient->reset();
self::assertNotSame($initialHandleId, (int) $clientState->handles[0]);
self::assertNotSame($initialShareId, $clientState->share);
}
public function testProcessAfterReset()