Merge branch '5.0' into 5.1

* 5.0:
  Fix test that fails on old distros
  Fix: compatibility with phpunit 9.3
  [DoctrineBridge] work around Connection::ping() deprecation
  [MimeType] Duplicated MimeType due to PHP Bug
  [DI] fix parsing of argument type=binary in xml
  fix guessing form types for DateTime types
  fix handling typed properties as constraint options
  Fix the 'supports' method argument type of the security voter
  Use the driverConnection executeUpdate method
This commit is contained in:
Nicolas Grekas 2020-06-28 17:32:35 +02:00
commit 4ad5079082
16 changed files with 212 additions and 23 deletions

View File

@ -11,6 +11,7 @@
namespace Symfony\Bridge\Doctrine\Messenger;
use Doctrine\DBAL\DBALException;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Middleware\StackInterface;
@ -36,7 +37,9 @@ class DoctrinePingConnectionMiddleware extends AbstractDoctrineMiddleware
{
$connection = $entityManager->getConnection();
if (!$connection->ping()) {
try {
$connection->query($connection->getDatabasePlatform()->getDummySelectSQL());
} catch (DBALException $e) {
$connection->close();
$connection->connect();
}

View File

@ -12,6 +12,7 @@
namespace Symfony\Bridge\Doctrine\Tests\Messenger;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DBALException;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Persistence\ManagerRegistry;
use Symfony\Bridge\Doctrine\Messenger\DoctrinePingConnectionMiddleware;
@ -47,8 +48,8 @@ class DoctrinePingConnectionMiddlewareTest extends MiddlewareTestCase
public function testMiddlewarePingOk()
{
$this->connection->expects($this->once())
->method('ping')
->willReturn(false);
->method('getDatabasePlatform')
->will($this->throwException(new DBALException()));
$this->connection->expects($this->once())
->method('close')
@ -65,6 +66,10 @@ class DoctrinePingConnectionMiddlewareTest extends MiddlewareTestCase
public function testMiddlewarePingResetEntityManager()
{
$this->connection->expects($this->once())
->method('getDatabasePlatform')
->will($this->throwException(new DBALException()));
$this->entityManager->expects($this->once())
->method('isOpen')
->willReturn(false)

View File

@ -12,9 +12,11 @@
namespace Symfony\Bridge\PhpUnit\Legacy;
use PHPUnit\TextUI\Command as BaseCommand;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Configuration\Configuration as LegacyConfiguration;
use PHPUnit\TextUI\Configuration\Registry;
use PHPUnit\TextUI\TestRunner as BaseRunner;
use PHPUnit\TextUI\XmlConfiguration\Configuration;
use PHPUnit\TextUI\XmlConfiguration\Loader;
use Symfony\Bridge\PhpUnit\SymfonyTestsListener;
/**
@ -43,9 +45,13 @@ class CommandForV9 extends BaseCommand
if (isset($this->arguments['configuration'])) {
$configuration = $this->arguments['configuration'];
if (!$configuration instanceof Configuration) {
if (!class_exists(Configuration::class) && !$configuration instanceof LegacyConfiguration) {
$configuration = Registry::getInstance()->get($this->arguments['configuration']);
} elseif (class_exists(Configuration::class) && !$configuration instanceof Configuration) {
$configuration = (new Loader())->load($this->arguments['configuration']);
}
foreach ($configuration->listeners() as $registeredListener) {
if ('Symfony\Bridge\PhpUnit\SymfonyTestsListener' === ltrim($registeredListener->className(), '\\')) {
$registeredLocally = true;

View File

@ -289,6 +289,7 @@
<xsd:enumeration value="expression" />
<xsd:enumeration value="string" />
<xsd:enumeration value="constant" />
<xsd:enumeration value="binary" />
<xsd:enumeration value="iterator" />
<xsd:enumeration value="service_locator" />
<!-- "tagged" is an alias of "tagged_iterator", using "tagged_iterator" is preferred. -->

View File

@ -97,6 +97,7 @@ class ValidatorTypeGuesser implements FormTypeGuesserInterface
case 'long':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\IntegerType', [], Guess::MEDIUM_CONFIDENCE);
case \DateTime::class:
case '\DateTime':
return new TypeGuess('Symfony\Component\Form\Extension\Core\Type\DateType', [], Guess::MEDIUM_CONFIDENCE);

View File

@ -12,9 +12,17 @@
namespace Symfony\Component\Form\Tests\Extension\Validator;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
use Symfony\Component\Form\Extension\Core\Type\DateType;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\Extension\Core\Type\NumberType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\Extension\Validator\ValidatorTypeGuesser;
use Symfony\Component\Form\Guess\Guess;
use Symfony\Component\Form\Guess\TypeGuess;
use Symfony\Component\Form\Guess\ValueGuess;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Email;
use Symfony\Component\Validator\Constraints\File;
use Symfony\Component\Validator\Constraints\IsTrue;
@ -60,6 +68,35 @@ class ValidatorTypeGuesserTest extends TestCase
$this->guesser = new ValidatorTypeGuesser($this->metadataFactory);
}
/**
* @dataProvider guessTypeProvider
*/
public function testGuessType(Constraint $constraint, TypeGuess $guess)
{
$this->metadata->addPropertyConstraint(self::TEST_PROPERTY, $constraint);
$this->assertEquals($guess, $this->guesser->guessType(self::TEST_CLASS, self::TEST_PROPERTY));
}
public function guessTypeProvider()
{
return [
[new Type('array'), new TypeGuess(CollectionType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('bool'), new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('boolean'), new TypeGuess(CheckboxType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('double'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('float'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('numeric'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('real'), new TypeGuess(NumberType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('int'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('integer'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('long'), new TypeGuess(IntegerType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('string'), new TypeGuess(TextType::class, [], Guess::LOW_CONFIDENCE)],
[new Type(\DateTime::class), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)],
[new Type('\DateTime'), new TypeGuess(DateType::class, [], Guess::MEDIUM_CONFIDENCE)],
];
}
public function guessRequiredProvider()
{
return [

View File

@ -137,7 +137,7 @@ class Connection implements ResetInterface
'available_at' => '?',
]);
$this->executeQuery($queryBuilder->getSQL(), [
$this->executeUpdate($queryBuilder->getSQL(), [
$body,
json_encode($headers),
$this->configuration['queue_name'],
@ -194,7 +194,7 @@ class Connection implements ResetInterface
->set('delivered_at', '?')
->where('id = ?');
$now = new \DateTime();
$this->executeQuery($queryBuilder->getSQL(), [
$this->executeUpdate($queryBuilder->getSQL(), [
$now,
$doctrineEnvelope['id'],
], [
@ -369,6 +369,25 @@ class Connection implements ResetInterface
return $stmt;
}
private function executeUpdate(string $sql, array $parameters = [], array $types = [])
{
try {
$stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types);
} catch (TableNotFoundException $e) {
if ($this->driverConnection->isTransactionActive()) {
throw $e;
}
// create table
if ($this->autoSetup) {
$this->setup();
}
$stmt = $this->driverConnection->executeUpdate($sql, $parameters, $types);
}
return $stmt;
}
private function getSchema(): Schema
{
$schema = new Schema([], [], $this->driverConnection->getSchemaManager()->createSchemaConfig());

View File

@ -57,7 +57,13 @@ class FileinfoMimeTypeGuesser implements MimeTypeGuesserInterface
if (false === $finfo = new \finfo(FILEINFO_MIME_TYPE, $this->magicFile)) {
return null;
}
$mimeType = $finfo->file($path);
return $finfo->file($path);
if ($mimeType && 0 === (\strlen($mimeType) % 2)) {
$mimeStart = substr($mimeType, 0, \strlen($mimeType) >> 1);
$mimeType = $mimeStart.$mimeStart === $mimeType ? $mimeStart : $mimeType;
}
return $mimeType;
}
}

View File

@ -79,6 +79,11 @@ abstract class AbstractMimeTypeGuesserTest extends TestCase
$this->assertEquals('application/octet-stream', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/mimetypes/.unknownextension'));
}
public function testGuessWithDuplicatedFileType()
{
$this->assertEquals('application/vnd.openxmlformats-officedocument.wordprocessingml.document', $this->getGuesser()->guessMimeType(__DIR__.'/Fixtures/test.docx'));
}
public function testGuessWithIncorrectPath()
{
if (!$this->getGuesser()->isGuesserSupported()) {

Binary file not shown.

View File

@ -30,8 +30,21 @@ abstract class Voter implements VoterInterface
$vote = self::ACCESS_ABSTAIN;
foreach ($attributes as $attribute) {
if (!$this->supports($attribute, $subject)) {
continue;
try {
if (!$this->supports($attribute, $subject)) {
continue;
}
} catch (\TypeError $e) {
if (\PHP_VERSION_ID < 80000) {
if (0 === strpos($e->getMessage(), 'Argument 1 passed to')
&& false !== strpos($e->getMessage(), '::supports() must be of the type string')) {
continue;
}
} elseif (false !== strpos($e->getMessage(), 'supports(): Argument #1')) {
continue;
}
throw $e;
}
// as soon as at least one attribute is supported, default is to deny access

View File

@ -27,34 +27,49 @@ class VoterTest extends TestCase
public function getTests()
{
$voter = new VoterTest_Voter();
$integerVoter = new IntegerVoterTest_Voter();
return [
[['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
[['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'],
[$voter, ['EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
[$voter, ['CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if attribute and class are supported and attribute does not grant access'],
[['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access'],
[['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access'],
[$voter, ['DELETE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute is supported and grants access'],
[$voter, ['DELETE', 'CREATE'], VoterInterface::ACCESS_DENIED, new \stdClass(), 'ACCESS_DENIED if one attribute is supported and denies access'],
[['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access'],
[$voter, ['CREATE', 'EDIT'], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if one attribute grants access'],
[['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported'],
[$voter, ['DELETE'], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attribute is supported'],
[['EDIT'], VoterInterface::ACCESS_ABSTAIN, $this, 'ACCESS_ABSTAIN if class is not supported'],
[$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, $this, 'ACCESS_ABSTAIN if class is not supported'],
[['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null'],
[$voter, ['EDIT'], VoterInterface::ACCESS_ABSTAIN, null, 'ACCESS_ABSTAIN if object is null'],
[[], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided'],
[$voter, [], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if no attributes were provided'],
[$voter, [new StringableAttribute()], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute and class are supported and attribute grants access'],
[$voter, [new \stdClass()], VoterInterface::ACCESS_ABSTAIN, new \stdClass(), 'ACCESS_ABSTAIN if attributes were not strings'],
[$integerVoter, [42], VoterInterface::ACCESS_GRANTED, new \stdClass(), 'ACCESS_GRANTED if attribute is an integer'],
];
}
/**
* @dataProvider getTests
*/
public function testVote(array $attributes, $expectedVote, $object, $message)
public function testVote(VoterInterface $voter, array $attributes, $expectedVote, $object, $message)
{
$voter = new VoterTest_Voter();
$this->assertEquals($expectedVote, $voter->vote($this->token, $object, $attributes), $message);
}
public function testVoteWithTypeError()
{
$this->expectException('TypeError');
$this->expectExceptionMessage('Should error');
$voter = new TypeErrorVoterTest_Voter();
$voter->vote($this->token, new \stdClass(), ['EDIT']);
}
}
class VoterTest_Voter extends Voter
@ -69,3 +84,37 @@ class VoterTest_Voter extends Voter
return $object instanceof \stdClass && \in_array($attribute, ['EDIT', 'CREATE']);
}
}
class IntegerVoterTest_Voter extends Voter
{
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool
{
return 42 === $attribute;
}
protected function supports($attribute, $object): bool
{
return $object instanceof \stdClass && \is_int($attribute);
}
}
class TypeErrorVoterTest_Voter extends Voter
{
protected function voteOnAttribute($attribute, $object, TokenInterface $token): bool
{
return false;
}
protected function supports($attribute, $object): bool
{
throw new \TypeError('Should error');
}
}
class StringableAttribute
{
public function __toString(): string
{
return 'EDIT';
}
}

View File

@ -116,7 +116,7 @@ abstract class Constraint
$defaultOption = $this->getDefaultOption();
$invalidOptions = [];
$missingOptions = array_flip((array) $this->getRequiredOptions());
$knownOptions = get_object_vars($this);
$knownOptions = get_class_vars(static::class);
// The "groups" option is added to the object lazily
$knownOptions['groups'] = true;

View File

@ -13,10 +13,13 @@ namespace Symfony\Component\Validator\Tests;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Exception\InvalidOptionsException;
use Symfony\Component\Validator\Tests\Fixtures\ClassConstraint;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintA;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintB;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintC;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithStaticProperty;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithTypedProperty;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValue;
use Symfony\Component\Validator\Tests\Fixtures\ConstraintWithValueAsDefault;
@ -245,4 +248,25 @@ class ConstraintTest extends TestCase
$this->expectExceptionMessage('No default option is configured for constraint "Symfony\Component\Validator\Tests\Fixtures\ConstraintB".');
new ConstraintB(['value' => 1]);
}
public function testStaticPropertiesAreNoOptions()
{
$this->expectException(InvalidOptionsException::class);
new ConstraintWithStaticProperty([
'foo' => 'bar',
]);
}
/**
* @requires PHP 7.4
*/
public function testSetTypedProperty()
{
$constraint = new ConstraintWithTypedProperty([
'foo' => 'bar',
]);
$this->assertSame('bar', $constraint->foo);
}
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\Validator\Tests\Fixtures;
use Symfony\Component\Validator\Constraint;
class ConstraintWithStaticProperty extends Constraint
{
public static $foo;
}

View File

@ -0,0 +1,10 @@
<?php
namespace Symfony\Component\Validator\Tests\Fixtures;
use Symfony\Component\Validator\Constraint;
class ConstraintWithTypedProperty extends Constraint
{
public string $foo;
}