Merge branch '4.4'

* 4.4:
  Fix inconsistency in json format regarding DST value
  changed type hints
  conflict with HttpKernel component 5.0
  do not process private properties from parent class
  [HttpClient] fix unregistering the debug buffer when using curl
  don't add embedded properties to wrapping class metadata
  [Messenger] set amqp content_type based on serialization format
  [Mailer] fixed the possibility to set a From header from MessageListener
This commit is contained in:
Fabien Potencier 2019-06-04 14:24:56 +02:00
commit 1177a2aeed
16 changed files with 306 additions and 71 deletions

View File

@ -0,0 +1,25 @@
<?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\Embeddable
*/
class DoctrineLoaderEmbed
{
/**
* @ORM\Column(length=25)
*/
public $embeddedMaxLength;
}

View File

@ -21,7 +21,7 @@ use Symfony\Component\Validator\Constraints as Assert;
*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class DoctrineLoaderEntity
class DoctrineLoaderEntity extends DoctrineLoaderParentEntity
{
/**
* @ORM\Id
@ -55,4 +55,9 @@ class DoctrineLoaderEntity
* @ORM\Column(unique=true)
*/
public $alreadyMappedUnique;
/**
* @ORM\Embedded(class=DoctrineLoaderEmbed::class)
*/
public $embedded;
}

View File

@ -0,0 +1,40 @@
<?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\MappedSuperclass
*/
class DoctrineLoaderParentEntity
{
/**
* @ORM\Column(length=35)
*/
public $publicParentMaxLength;
/**
* @ORM\Column(length=30)
*/
private $privateParentMaxLength;
public function getPrivateParentMaxLength()
{
return $this->privateParentMaxLength;
}
public function setPrivateParentMaxLength($privateParentMaxLength): void
{
$this->privateParentMaxLength = $privateParentMaxLength;
}
}

View File

@ -14,11 +14,15 @@ namespace Symfony\Bridge\Doctrine\Tests\Validator;
use PHPUnit\Framework\TestCase;
use Symfony\Bridge\Doctrine\Test\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\DoctrineLoaderParentEntity;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Bridge\Doctrine\Validator\DoctrineLoader;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Mapping\CascadingStrategy;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\TraversalStrategy;
use Symfony\Component\Validator\Tests\Fixtures\Entity;
use Symfony\Component\Validator\Validation;
use Symfony\Component\Validator\ValidatorBuilder;
@ -36,7 +40,7 @@ class DoctrineLoaderTest extends TestCase
$validator = Validation::createValidatorBuilder()
->enableAnnotationMapping()
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoaderEntity$}'))
->addLoader(new DoctrineLoader(DoctrineTestHelper::createTestEntityManager(), '{^Symfony\\\\Bridge\\\\Doctrine\\\\Tests\\\\Fixtures\\\\DoctrineLoader}'))
->getValidator()
;
@ -71,6 +75,39 @@ class DoctrineLoaderTest extends TestCase
$this->assertInstanceOf(Length::class, $alreadyMappedMaxLengthConstraints[0]);
$this->assertSame(10, $alreadyMappedMaxLengthConstraints[0]->max);
$this->assertSame(1, $alreadyMappedMaxLengthConstraints[0]->min);
$publicParentMaxLengthMetadata = $classMetadata->getPropertyMetadata('publicParentMaxLength');
$this->assertCount(1, $publicParentMaxLengthMetadata);
$publicParentMaxLengthConstraints = $publicParentMaxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $publicParentMaxLengthConstraints);
$this->assertInstanceOf(Length::class, $publicParentMaxLengthConstraints[0]);
$this->assertSame(35, $publicParentMaxLengthConstraints[0]->max);
$embeddedMetadata = $classMetadata->getPropertyMetadata('embedded');
$this->assertCount(1, $embeddedMetadata);
$this->assertSame(CascadingStrategy::CASCADE, $embeddedMetadata[0]->getCascadingStrategy());
$this->assertSame(TraversalStrategy::IMPLICIT, $embeddedMetadata[0]->getTraversalStrategy());
$parentClassMetadata = $validator->getMetadataFor(new DoctrineLoaderParentEntity());
$publicParentMaxLengthMetadata = $parentClassMetadata->getPropertyMetadata('publicParentMaxLength');
$this->assertCount(0, $publicParentMaxLengthMetadata);
$privateParentMaxLengthMetadata = $parentClassMetadata->getPropertyMetadata('privateParentMaxLength');
$this->assertCount(1, $privateParentMaxLengthMetadata);
$privateParentMaxLengthConstraints = $privateParentMaxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $privateParentMaxLengthConstraints);
$this->assertInstanceOf(Length::class, $privateParentMaxLengthConstraints[0]);
$this->assertSame(30, $privateParentMaxLengthConstraints[0]->max);
$embeddedClassMetadata = $validator->getMetadataFor(new DoctrineLoaderEmbed());
$embeddedMaxLengthMetadata = $embeddedClassMetadata->getPropertyMetadata('embeddedMaxLength');
$this->assertCount(1, $embeddedMaxLengthMetadata);
$embeddedMaxLengthConstraints = $embeddedMaxLengthMetadata[0]->getConstraints();
$this->assertCount(1, $embeddedMaxLengthConstraints);
$this->assertInstanceOf(Length::class, $embeddedMaxLengthConstraints[0]);
$this->assertSame(25, $embeddedMaxLengthConstraints[0]->max);
}
public function testFieldMappingsConfiguration()

View File

@ -17,6 +17,7 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo;
use Doctrine\ORM\Mapping\MappingException as OrmMappingException;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\Mapping\ClassMetadata;
use Symfony\Component\Validator\Mapping\Loader\LoaderInterface;
@ -78,7 +79,11 @@ final class DoctrineLoader implements LoaderInterface
$constraint = $this->getLengthConstraint($metadata, $mapping['fieldName']);
if (null === $constraint) {
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
if (isset($mapping['originalClass'])) {
$metadata->addPropertyConstraint($mapping['declaredField'], new Valid());
} elseif (property_exists($className, $mapping['fieldName'])) {
$metadata->addPropertyConstraint($mapping['fieldName'], new Length(['max' => $mapping['length']]));
}
} elseif (null === $constraint->max) {
// If a Length constraint exists and no max length has been explicitly defined, set it
$constraint->max = $mapping['length'];

View File

@ -167,6 +167,7 @@ final class CurlResponse implements ResponseInterface
if (!\in_array(curl_getinfo($this->handle, CURLINFO_PRIVATE), ['headers', 'content'], true)) {
rewind($this->debugBuffer);
$info['debug'] = stream_get_contents($this->debugBuffer);
curl_setopt($this->handle, CURLOPT_VERBOSE, false);
fclose($this->debugBuffer);
$this->debugBuffer = null;
$this->finalInfo = $info;

View File

@ -0,0 +1,98 @@
<?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\Component\Mailer;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Message;
/**
* @author Fabien Potencier <fabien@symfony.com>
*
* @experimental in 4.3
*
* @internal
*/
final class DelayedSmtpEnvelope extends SmtpEnvelope
{
private $senderSet = false;
private $recipientsSet = false;
private $message;
public function __construct(Message $message)
{
$this->message = $message;
}
public function setSender(Address $sender): void
{
parent::setSender($sender);
$this->senderSet = true;
}
public function getSender(): Address
{
if ($this->senderSet) {
return parent::getSender();
}
return self::getSenderFromHeaders($this->message->getHeaders());
}
public function setRecipients(array $recipients): void
{
parent::setRecipients($recipients);
$this->recipientsSet = parent::getRecipients();
}
/**
* @return Address[]
*/
public function getRecipients(): array
{
if ($this->recipientsSet) {
return parent::getRecipients();
}
return self::getRecipientsFromHeaders($this->message->getHeaders());
}
private static function getRecipientsFromHeaders(Headers $headers): array
{
$recipients = [];
foreach (['to', 'cc', 'bcc'] as $name) {
foreach ($headers->getAll($name) as $header) {
$recipients = array_merge($recipients, $header->getAddresses());
}
}
return $recipients;
}
private static function getSenderFromHeaders(Headers $headers): Address
{
if ($return = $headers->get('Return-Path')) {
return $return->getAddress();
}
if ($sender = $headers->get('Sender')) {
return $sender->getAddress();
}
if ($from = $headers->get('From')) {
return $from->getAddresses()[0];
}
throw new LogicException('Unable to determine the sender of the message.');
}
}

View File

@ -12,10 +12,7 @@
namespace Symfony\Component\Mailer;
use Symfony\Component\Mailer\Exception\InvalidArgumentException;
use Symfony\Component\Mailer\Exception\LogicException;
use Symfony\Component\Mime\Address;
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\NamedAddress;
use Symfony\Component\Mime\RawMessage;
@ -40,14 +37,7 @@ class SmtpEnvelope
public static function create(RawMessage $message): self
{
if ($message instanceof Message) {
$headers = $message->getHeaders();
return new self(self::getSenderFromHeaders($headers), self::getRecipientsFromHeaders($headers));
}
// FIXME: parse the raw message to create the envelope?
throw new InvalidArgumentException(sprintf('Unable to create an SmtpEnvelope from a "%s" message.', RawMessage::class));
return new DelayedSmtpEnvelope($message);
}
public function setSender(Address $sender): void
@ -84,31 +74,4 @@ class SmtpEnvelope
{
return $this->recipients;
}
private static function getRecipientsFromHeaders(Headers $headers): array
{
$recipients = [];
foreach (['to', 'cc', 'bcc'] as $name) {
foreach ($headers->getAll($name) as $header) {
$recipients = array_merge($recipients, $header->getAddresses());
}
}
return $recipients;
}
private static function getSenderFromHeaders(Headers $headers): Address
{
if ($return = $headers->get('Return-Path')) {
return $return->getAddress();
}
if ($sender = $headers->get('Sender')) {
return $sender->getAddress();
}
if ($from = $headers->get('From')) {
return $from->getAddresses()[0];
}
throw new LogicException('Unable to determine the sender of the message.');
}
}

View File

@ -72,12 +72,13 @@ class SmtpEnvelopeTest extends TestCase
$this->assertEquals('from@symfony.com', $e->getSender()->getAddress());
}
public function testSenderFromHeadersWithoutData()
public function testSenderFromHeadersWithoutFrom()
{
$this->expectException(\LogicException::class);
$headers = new Headers();
$headers->addMailboxListHeader('To', ['from@symfony.com']);
SmtpEnvelope::create(new Message($headers));
$e = SmtpEnvelope::create($message = new Message($headers));
$message->getHeaders()->addMailboxListHeader('From', ['from@symfony.com']);
$this->assertEquals('from@symfony.com', $e->getSender()->getAddress());
}
public function testRecipientsFromHeaders()
@ -90,10 +91,4 @@ class SmtpEnvelopeTest extends TestCase
$e = SmtpEnvelope::create(new Message($headers));
$this->assertEquals([new Address('to@symfony.com'), new Address('cc@symfony.com'), new Address('bcc@symfony.com')], $e->getRecipients());
}
public function testCreateWithRawMessage()
{
$this->expectException(\InvalidArgumentException::class);
SmtpEnvelope::create(new RawMessage(''));
}
}

View File

@ -15,6 +15,7 @@ use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Mailer\DelayedSmtpEnvelope;
use Symfony\Component\Mailer\Event\MessageEvent;
use Symfony\Component\Mailer\Exception\TransportException;
use Symfony\Component\Mailer\SentMessage;
@ -62,7 +63,7 @@ abstract class AbstractTransport implements TransportInterface
$envelope = clone $envelope;
} else {
try {
$envelope = SmtpEnvelope::create($message);
$envelope = new DelayedSmtpEnvelope($message);
} catch (\Exception $e) {
throw new TransportException('Cannot send message without a valid envelope.', 0, $e);
}

View File

@ -69,6 +69,39 @@ class AmqpSenderTest extends TestCase
$sender->send($envelope);
}
public function testContentTypeHeaderIsMovedToAttribute()
{
$envelope = new Envelope(new DummyMessage('Oy'));
$encoded = ['body' => '...', 'headers' => ['type' => DummyMessage::class, 'Content-Type' => 'application/json']];
$serializer = $this->getMockBuilder(SerializerInterface::class)->getMock();
$serializer->method('encode')->with($envelope)->willReturnOnConsecutiveCalls($encoded);
$connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
unset($encoded['headers']['Content-Type']);
$stamp = new AmqpStamp(null, AMQP_NOPARAM, ['content_type' => 'application/json']);
$connection->expects($this->once())->method('publish')->with($encoded['body'], $encoded['headers'], 0, $stamp);
$sender = new AmqpSender($connection, $serializer);
$sender->send($envelope);
}
public function testContentTypeHeaderDoesNotOverwriteAttribute()
{
$envelope = (new Envelope(new DummyMessage('Oy')))->with($stamp = new AmqpStamp('rk', AMQP_NOPARAM, ['content_type' => 'custom']));
$encoded = ['body' => '...', 'headers' => ['type' => DummyMessage::class, 'Content-Type' => 'application/json']];
$serializer = $this->getMockBuilder(SerializerInterface::class)->getMock();
$serializer->method('encode')->with($envelope)->willReturnOnConsecutiveCalls($encoded);
$connection = $this->getMockBuilder(Connection::class)->disableOriginalConstructor()->getMock();
unset($encoded['headers']['Content-Type']);
$connection->expects($this->once())->method('publish')->with($encoded['body'], $encoded['headers'], 0, $stamp);
$sender = new AmqpSender($connection, $serializer);
$sender->send($envelope);
}
/**
* @expectedException \Symfony\Component\Messenger\Exception\TransportException
*/

View File

@ -55,7 +55,8 @@ class SerializerTest extends TestCase
$this->assertArrayHasKey('body', $encoded);
$this->assertArrayHasKey('headers', $encoded);
$this->assertArrayHasKey('type', $encoded['headers']);
$this->assertEquals(DummyMessage::class, $encoded['headers']['type']);
$this->assertSame(DummyMessage::class, $encoded['headers']['type']);
$this->assertSame('application/json', $encoded['headers']['Content-Type']);
}
public function testUsesTheCustomFormatAndContext()

View File

@ -50,12 +50,26 @@ class AmqpSender implements SenderInterface
$delay = $delayStamp->getDelay();
}
$amqpStamp = $envelope->last(AmqpStamp::class);
if (isset($encodedMessage['headers']['Content-Type'])) {
$contentType = $encodedMessage['headers']['Content-Type'];
unset($encodedMessage['headers']['Content-Type']);
$attributes = $amqpStamp ? $amqpStamp->getAttributes() : [];
if (!isset($attributes['content_type'])) {
$attributes['content_type'] = $contentType;
$amqpStamp = new AmqpStamp($amqpStamp ? $amqpStamp->getRoutingKey() : null, $amqpStamp ? $amqpStamp->getFlags() : AMQP_NOPARAM, $attributes);
}
}
try {
$this->connection->publish(
$encodedMessage['body'],
$encodedMessage['headers'] ?? [],
$delay,
$envelope->last(AmqpStamp::class)
$amqpStamp
);
} catch (\AMQPException $e) {
throw new TransportException($e->getMessage(), 0, $e);

View File

@ -101,7 +101,7 @@ class Serializer implements SerializerInterface
$envelope = $envelope->withoutStampsOfType(NonSendableStampInterface::class);
$headers = ['type' => \get_class($envelope->getMessage())] + $this->encodeStamps($envelope);
$headers = ['type' => \get_class($envelope->getMessage())] + $this->encodeStamps($envelope) + $this->getContentTypeHeader();
return [
'body' => $this->serializer->serialize($envelope->getMessage(), $this->format, $context),
@ -157,4 +157,28 @@ class Serializer implements SerializerInterface
return null;
}
private function getContentTypeHeader(): array
{
$mimeType = $this->getMimeTypeForFormat();
return null === $mimeType ? [] : ['Content-Type' => $mimeType];
}
private function getMimeTypeForFormat(): ?string
{
switch ($this->format) {
case 'json':
return 'application/json';
case 'xml':
return 'application/xml';
case 'yml':
case 'yaml':
return 'application/x-yaml';
case 'csv':
return 'text/csv';
}
return null;
}
}

View File

@ -28,17 +28,12 @@ final class MessageConverter
/**
* @throws RuntimeException when unable to convert the message to an email
*/
public static function toEmail(RawMessage $message): Email
public static function toEmail(Message $message): Email
{
if ($message instanceof Email) {
return $message;
}
if (RawMessage::class === \get_class($message)) {
// FIXME: parse the raw message to create the envelope?
throw new RuntimeException(sprintf('Unable to create an Email from an instance of "%s" as it is not supported yet.', RawMessage::class));
}
// try to convert to a "simple" Email instance
$body = $message->getBody();
if ($body instanceof TextPart) {

View File

@ -240,6 +240,12 @@ EOTXT;
$expectedTimeType = $var->getTimeType();
$expectedDateType = $var->getDateType();
$expectedTimeZone = $var->getTimeZone();
$expectedTimeZoneDisplayName = $expectedTimeZone->getDisplayName();
$expectedTimeZoneID = $expectedTimeZone->getID();
$expectedTimeZoneRawOffset = $expectedTimeZone->getRawOffset();
$expectedTimeZoneDSTSavings = $expectedTimeZone->useDaylightTime() ? "\n dst_savings: ".$expectedTimeZone->getDSTSavings() : '';
$expectedCalendarObject = $var->getCalendarObject();
$expectedCalendarObjectType = $expectedCalendarObject->getType();
$expectedCalendarObjectFirstDayOfWeek = $expectedCalendarObject->getFirstDayOfWeek();
@ -254,13 +260,7 @@ EOTXT;
$expectedCalendarObjectTimeZoneDisplayName = $expectedCalendarObjectTimeZone->getDisplayName();
$expectedCalendarObjectTimeZoneID = $expectedCalendarObjectTimeZone->getID();
$expectedCalendarObjectTimeZoneRawOffset = $expectedCalendarObjectTimeZone->getRawOffset();
$expectedCalendarObjectTimeZoneDSTSavings = $expectedCalendarObjectTimeZone->getDSTSavings();
$expectedTimeZone = $var->getTimeZone();
$expectedTimeZoneDisplayName = $expectedTimeZone->getDisplayName();
$expectedTimeZoneID = $expectedTimeZone->getID();
$expectedTimeZoneRawOffset = $expectedTimeZone->getRawOffset();
$expectedTimeZoneDSTSavings = $expectedTimeZone->getDSTSavings();
$expectedCalendarObjectTimeZoneDSTSavings = $expectedTimeZone->useDaylightTime() ? "\n dst_savings: ".$expectedCalendarObjectTimeZone->getDSTSavings() : '';
$expected = <<<EOTXT
IntlDateFormatter {
@ -282,15 +282,13 @@ IntlDateFormatter {
time_zone: IntlTimeZone {
display_name: "$expectedCalendarObjectTimeZoneDisplayName"
id: "$expectedCalendarObjectTimeZoneID"
raw_offset: $expectedCalendarObjectTimeZoneRawOffset
dst_savings: $expectedCalendarObjectTimeZoneDSTSavings
raw_offset: $expectedCalendarObjectTimeZoneRawOffset$expectedCalendarObjectTimeZoneDSTSavings
}
}
time_zone: IntlTimeZone {
display_name: "$expectedTimeZoneDisplayName"
id: "$expectedTimeZoneID"
raw_offset: $expectedTimeZoneRawOffset
dst_savings: $expectedTimeZoneDSTSavings
raw_offset: $expectedTimeZoneRawOffset$expectedTimeZoneDSTSavings
}
}
EOTXT;