Merge branch '3.4' into 4.2

* 3.4:
  Fix expired lock not cleaned
  [HttpFoundation] Fix SA/phpdoc JsonResponse
  SimpleCacheAdapter fails to cache any item if a namespace is used
  validate composite constraints in all groups
  [Serializer] Handle true and false appropriately in CSV encoder
  Fix binary operation `+`, `-` or `*` on string
  [VarDumper] fix dumping objects that implement __debugInfo()
  [Routing] fix absolute url generation when scheme is not known
This commit is contained in:
Nicolas Grekas 2019-06-17 19:15:38 +02:00
commit 06c17cdaa9
28 changed files with 285 additions and 68 deletions

View File

@ -38,7 +38,7 @@ abstract class AbstractAdapter implements AdapterInterface, CacheInterface, Logg
protected function __construct(string $namespace = '', int $defaultLifetime = 0)
{
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).':';
$this->namespace = '' === $namespace ? '' : CacheItem::validateKey($namespace).static::getNsSeparator();
if (null !== $this->maxIdLength && \strlen($namespace) > $this->maxIdLength - 24) {
throw new InvalidArgumentException(sprintf('Namespace must be %d chars max, %d given ("%s")', $this->maxIdLength - 24, \strlen($namespace), $namespace));
}

View File

@ -75,4 +75,12 @@ class SimpleCacheAdapter extends AbstractAdapter implements PruneableInterface
{
return $this->pool->setMultiple($values, 0 === $lifetime ? null : $lifetime);
}
/**
* @return string the namespace separator for cache keys
*/
protected static function getNsSeparator()
{
return '_';
}
}

View File

@ -56,7 +56,7 @@ class MaxIdLengthAdapterTest extends TestCase
$reflectionProperty->setValue($cache, true);
// Versioning enabled
$this->assertEquals('--------------------------:1/------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]));
$this->assertEquals('--------------------------:1:------------', $reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)]));
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 12)])));
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 23)])));
$this->assertLessThanOrEqual(50, \strlen($reflectionMethod->invokeArgs($cache, [str_repeat('-', 40)])));

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\Tests\Adapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\SimpleCacheAdapter;
use Symfony\Component\Cache\Simple\ArrayCache;
use Symfony\Component\Cache\Simple\Psr6Cache;
/**
@ -28,4 +29,14 @@ class SimpleCacheAdapterTest extends AdapterTestCase
{
return new SimpleCacheAdapter(new Psr6Cache(new FilesystemAdapter()), '', $defaultLifetime);
}
public function testValidCacheKeyWithNamespace()
{
$cache = new SimpleCacheAdapter(new ArrayCache(), 'some_namespace', 0);
$item = $cache->getItem('my_key');
$item->set('someValue');
$cache->save($item);
$this->assertTrue($cache->getItem('my_key')->isHit(), 'Stored item is successfully retrieved.');
}
}

View File

@ -107,9 +107,9 @@ trait AbstractTrait
{
$this->deferred = [];
if ($cleared = $this->versioningIsEnabled) {
$namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), ':', 5);
$namespaceVersion = substr_replace(base64_encode(pack('V', mt_rand())), static::getNsSeparator(), 5);
try {
$cleared = $this->doSave(['/'.$this->namespace => $namespaceVersion], 0);
$cleared = $this->doSave([static::getNsSeparator().$this->namespace => $namespaceVersion], 0);
} catch (\Exception $e) {
$cleared = false;
}
@ -242,14 +242,14 @@ trait AbstractTrait
{
if ($this->versioningIsEnabled && '' === $this->namespaceVersion) {
$this->ids = [];
$this->namespaceVersion = '1/';
$this->namespaceVersion = '1'.static::getNsSeparator();
try {
foreach ($this->doFetch(['/'.$this->namespace]) as $v) {
foreach ($this->doFetch([static::getNsSeparator().$this->namespace]) as $v) {
$this->namespaceVersion = $v;
}
if ('1:' === $this->namespaceVersion) {
$this->namespaceVersion = substr_replace(base64_encode(pack('V', time())), ':', 5);
$this->doSave(['@'.$this->namespace => $this->namespaceVersion], 0);
if ('1'.static::getNsSeparator() === $this->namespaceVersion) {
$this->namespaceVersion = substr_replace(base64_encode(pack('V', time())), static::getNsSeparator(), 5);
$this->doSave([static::getNsSeparator().$this->namespace => $this->namespaceVersion], 0);
}
} catch (\Exception $e) {
}
@ -266,7 +266,7 @@ trait AbstractTrait
}
if (\strlen($id = $this->namespace.$this->namespaceVersion.$key) > $this->maxIdLength) {
// Use MD5 to favor speed over security, which is not an issue here
$this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), ':', -(\strlen($this->namespaceVersion) + 2));
$this->ids[$key] = $id = substr_replace(base64_encode(hash('md5', $key, true)), static::getNsSeparator(), -(\strlen($this->namespaceVersion) + 2));
$id = $this->namespace.$this->namespaceVersion.$id;
}
@ -280,4 +280,12 @@ trait AbstractTrait
{
throw new \DomainException('Class not found: '.$class);
}
/**
* @return string the namespace separator for cache keys
*/
protected static function getNsSeparator()
{
return ':';
}
}

View File

@ -21,7 +21,7 @@ class BirthdayType extends AbstractType
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefault('years', range(date('Y') - 120, date('Y')));
$resolver->setDefault('years', range((int) date('Y') - 120, date('Y')));
$resolver->setAllowedTypes('years', 'array');
}

View File

@ -259,7 +259,7 @@ class DateType extends AbstractType
};
$resolver->setDefaults([
'years' => range(date('Y') - 5, date('Y') + 5),
'years' => range((int) date('Y') - 5, (int) date('Y') + 5),
'months' => range(1, 12),
'days' => range(1, 31),
'widget' => 'choice',

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Form\Extension\Validator\Constraints;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\Constraints\Composite;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\Valid;
use Symfony\Component\Validator\ConstraintValidator;
@ -90,7 +91,9 @@ class FormValidator extends ConstraintValidator
$validator->atPath('data')->validate($form->getData(), $constraint, $group);
// Prevent duplicate validation
continue 2;
if (!$constraint instanceof Composite) {
continue 2;
}
}
}
}

View File

@ -24,6 +24,7 @@ use Symfony\Component\Form\FormFactoryInterface;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\SubmitButtonBuilder;
use Symfony\Component\Translation\IdentityTranslator;
use Symfony\Component\Validator\Constraints\Collection;
use Symfony\Component\Validator\Constraints\GroupSequence;
use Symfony\Component\Validator\Constraints\NotBlank;
use Symfony\Component\Validator\Constraints\NotNull;
@ -673,6 +674,63 @@ class FormValidatorTest extends ConstraintValidatorTestCase
$this->assertSame($constraint, $context->getViolations()->get(0)->getConstraint());
}
public function testNonCompositeConstraintValidatedOnce()
{
$form = $this
->getBuilder('form', null, [
'constraints' => [new NotBlank(['groups' => ['foo', 'bar']])],
'validation_groups' => ['foo', 'bar'],
])
->setCompound(false)
->getForm();
$form->submit('');
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$this->validator->initialize($context);
$this->validator->validate($form, new Form());
$this->assertCount(1, $context->getViolations());
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
$this->assertSame('data', $context->getViolations()[0]->getPropertyPath());
}
public function testCompositeConstraintValidatedInEachGroup()
{
$form = $this->getBuilder('form', null, [
'constraints' => [
new Collection([
'field1' => new NotBlank([
'groups' => ['field1'],
]),
'field2' => new NotBlank([
'groups' => ['field2'],
]),
]),
],
'validation_groups' => ['field1', 'field2'],
])
->setData([])
->setCompound(true)
->setDataMapper(new PropertyPathMapper())
->getForm();
$form->add($this->getForm('field1'));
$form->add($this->getForm('field2'));
$form->submit([
'field1' => '',
'field2' => '',
]);
$context = new ExecutionContext(Validation::createValidator(), $form, new IdentityTranslator());
$this->validator->initialize($context);
$this->validator->validate($form, new Form());
$this->assertCount(2, $context->getViolations());
$this->assertSame('This value should not be blank.', $context->getViolations()[0]->getMessage());
$this->assertSame('data[field1]', $context->getViolations()[0]->getPropertyPath());
$this->assertSame('This value should not be blank.', $context->getViolations()[1]->getMessage());
$this->assertSame('data[field2]', $context->getViolations()[1]->getPropertyPath());
}
protected function createValidator()
{
return new FormValidator();

View File

@ -55,10 +55,10 @@ class JsonResponse extends Response
*
* Example:
*
* return JsonResponse::create($data, 200)
* return JsonResponse::create(['key' => 'value'])
* ->setSharedMaxAge(300);
*
* @param mixed $data The json response data
* @param mixed $data The JSON response data
* @param int $status The response status code
* @param array $headers An array of response headers
*
@ -70,7 +70,18 @@ class JsonResponse extends Response
}
/**
* Make easier the creation of JsonResponse from raw json.
* Factory method for chainability.
*
* Example:
*
* return JsonResponse::fromJsonString('{"key": "value"}')
* ->setSharedMaxAge(300);
*
* @param string|null $data The JSON response string
* @param int $status The response status code
* @param array $headers An array of response headers
*
* @return static
*/
public static function fromJsonString($data = null, $status = 200, $headers = [])
{

View File

@ -684,7 +684,7 @@ class Response
return (int) $age;
}
return max(time() - $this->getDate()->format('U'), 0);
return max(time() - (int) $this->getDate()->format('U'), 0);
}
/**
@ -764,7 +764,7 @@ class Response
}
if (null !== $this->getExpires()) {
return (int) ($this->getExpires()->format('U') - $this->getDate()->format('U'));
return (int) $this->getExpires()->format('U') - (int) $this->getDate()->format('U');
}
return null;

View File

@ -85,7 +85,7 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
$this->storeRelativeAgeDirective('s-maxage', $response->headers->getCacheControlDirective('s-maxage') ?: $response->headers->getCacheControlDirective('max-age'), $age);
$expires = $response->getExpires();
$expires = null !== $expires ? $expires->format('U') - $response->getDate()->format('U') : null;
$expires = null !== $expires ? (int) $expires->format('U') - (int) $response->getDate()->format('U') : null;
$this->storeRelativeAgeDirective('expires', $expires >= 0 ? $expires : null, 0);
}

View File

@ -25,7 +25,7 @@ class DayOfYearTransformer extends Transformer
*/
public function format(\DateTime $dateTime, int $length): string
{
$dayOfYear = $dateTime->format('z') + 1;
$dayOfYear = (int) $dateTime->format('z') + 1;
return $this->padLeft($dayOfYear, $length);
}

View File

@ -315,7 +315,7 @@ class FullTransformer
preg_match_all($this->regExp, $this->pattern, $matches);
if (\in_array('yy', $matches[0])) {
$dateTime->setTimestamp(time());
$year = $year > $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
$year = $year > (int) $dateTime->format('y') + 20 ? 1900 + $year : 2000 + $year;
}
$dateTime->setDate($year, $month, $day);

View File

@ -83,6 +83,11 @@ final class Lock implements LockInterface, LoggerAwareInterface
}
if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $this->key));
}
@ -120,6 +125,11 @@ final class Lock implements LockInterface, LoggerAwareInterface
$this->dirty = true;
if ($this->key->isExpired()) {
try {
$this->release();
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $this->key));
}

View File

@ -16,7 +16,6 @@ use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Exception\NotSupportedException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
@ -30,6 +29,7 @@ use Symfony\Component\Lock\Strategy\StrategyInterface;
class CombinedStore implements StoreInterface, LoggerAwareInterface
{
use LoggerAwareTrait;
use ExpiringStoreTrait;
/** @var StoreInterface[] */
private $stores;
@ -78,6 +78,8 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
}
}
$this->checkNotExpired($key);
if ($this->strategy->isMet($successCount, $storesCount)) {
return;
}
@ -125,9 +127,7 @@ class CombinedStore implements StoreInterface, LoggerAwareInterface
}
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
if ($this->strategy->isMet($successCount, $storesCount)) {
return;

View File

@ -0,0 +1,30 @@
<?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\Lock\Store;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
trait ExpiringStoreTrait
{
private function checkNotExpired(Key $key)
{
if ($key->isExpired()) {
try {
$this->delete($key);
} catch (\Exception $e) {
// swallow exception to not hide the original issue
}
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
}
}

View File

@ -13,7 +13,6 @@ namespace Symfony\Component\Lock\Store;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
@ -24,6 +23,8 @@ use Symfony\Component\Lock\StoreInterface;
*/
class MemcachedStore implements StoreInterface
{
use ExpiringStoreTrait;
private $memcached;
private $initialTtl;
/** @var bool */
@ -64,9 +65,7 @@ class MemcachedStore implements StoreInterface
$this->putOffExpiration($key, $this->initialTtl);
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}
public function waitAndSave(Key $key)
@ -110,9 +109,7 @@ class MemcachedStore implements StoreInterface
throw new LockConflictedException();
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
}
/**

View File

@ -15,7 +15,6 @@ use Symfony\Component\Cache\Traits\RedisClusterProxy;
use Symfony\Component\Cache\Traits\RedisProxy;
use Symfony\Component\Lock\Exception\InvalidArgumentException;
use Symfony\Component\Lock\Exception\LockConflictedException;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
@ -26,6 +25,8 @@ use Symfony\Component\Lock\StoreInterface;
*/
class RedisStore implements StoreInterface
{
use ExpiringStoreTrait;
private $redis;
private $initialTtl;
@ -67,9 +68,7 @@ class RedisStore implements StoreInterface
throw new LockConflictedException();
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to store the "%s" lock.', $key));
}
$this->checkNotExpired($key);
}
public function waitAndSave(Key $key)
@ -95,9 +94,7 @@ class RedisStore implements StoreInterface
throw new LockConflictedException();
}
if ($key->isExpired()) {
throw new LockExpiredException(sprintf('Failed to put off the expiration of the "%s" lock within the specified time.', $key));
}
$this->checkNotExpired($key);
}
/**

View File

@ -11,6 +11,7 @@
namespace Symfony\Component\Lock\Tests\Store;
use Symfony\Component\Lock\Exception\LockExpiredException;
use Symfony\Component\Lock\Key;
use Symfony\Component\Lock\StoreInterface;
@ -105,4 +106,28 @@ trait ExpiringStoreTestTrait
$this->assertGreaterThanOrEqual(0, $key->getRemainingLifetime());
$this->assertLessThanOrEqual(1, $key->getRemainingLifetime());
}
public function testExpiredLockCleaned()
{
$resource = uniqid(__METHOD__, true);
$key1 = new Key($resource);
$key2 = new Key($resource);
/** @var StoreInterface $store */
$store = $this->getStore();
$key1->reduceLifetime(0);
$this->assertTrue($key1->isExpired());
try {
$store->save($key1);
$this->fail('The store shouldn\'t have save an expired key');
} catch (LockExpiredException $e) {
}
$this->assertFalse($store->exists($key1));
$store->save($key2);
$this->assertTrue($store->exists($key2));
}
}

View File

@ -239,16 +239,18 @@ class UrlGenerator implements UrlGeneratorInterface, ConfigurableRequirementsInt
}
}
if ((self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) && !empty($host)) {
$port = '';
if ('http' === $scheme && 80 != $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 != $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
if (self::ABSOLUTE_URL === $referenceType || self::NETWORK_PATH === $referenceType) {
if ('' !== $host || ('' !== $scheme && 'http' !== $scheme && 'https' !== $scheme)) {
$port = '';
if ('http' === $scheme && 80 !== $this->context->getHttpPort()) {
$port = ':'.$this->context->getHttpPort();
} elseif ('https' === $scheme && 443 !== $this->context->getHttpsPort()) {
$port = ':'.$this->context->getHttpsPort();
}
$schemeAuthority = self::NETWORK_PATH === $referenceType ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
$schemeAuthority = self::NETWORK_PATH === $referenceType || '' === $scheme ? '//' : "$scheme://";
$schemeAuthority .= $host.$port;
}
}
if (self::RELATIVE_PATH === $referenceType) {

View File

@ -579,28 +579,27 @@ class UrlGeneratorTest extends TestCase
public function testDefaultHostIsUsedWhenContextHostIsEmpty()
{
$routes = $this->getRoutes('test', new Route('/route', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}', ['http']));
$routes = $this->getRoutes('test', new Route('/path', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}'));
$generator = $this->getGenerator($routes);
$generator->getContext()->setHost('');
$this->assertSame('http://my.fallback.host/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
$this->assertSame('http://my.fallback.host/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
}
public function testDefaultHostIsUsedWhenContextHostIsEmptyAndSchemeIsNot()
public function testDefaultHostIsUsedWhenContextHostIsEmptyAndPathReferenceType()
{
$routes = $this->getRoutes('test', new Route('/route', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}', ['http', 'https']));
$routes = $this->getRoutes('test', new Route('/path', ['domain' => 'my.fallback.host'], ['domain' => '.+'], [], '{domain}'));
$generator = $this->getGenerator($routes);
$generator->getContext()->setHost('');
$generator->getContext()->setScheme('https');
$this->assertSame('https://my.fallback.host/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
$this->assertSame('//my.fallback.host/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_PATH));
}
public function testAbsoluteUrlFallbackToRelativeIfHostIsEmptyAndSchemeIsNot()
public function testAbsoluteUrlFallbackToPathIfHostIsEmptyAndSchemeIsHttp()
{
$routes = $this->getRoutes('test', new Route('/route', [], [], [], '', ['http', 'https']));
$routes = $this->getRoutes('test', new Route('/route'));
$generator = $this->getGenerator($routes);
$generator->getContext()->setHost('');
@ -609,6 +608,39 @@ class UrlGeneratorTest extends TestCase
$this->assertSame('/app.php/route', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
}
public function testAbsoluteUrlFallbackToNetworkIfSchemeIsEmptyAndHostIsNot()
{
$routes = $this->getRoutes('test', new Route('/path'));
$generator = $this->getGenerator($routes);
$generator->getContext()->setHost('example.com');
$generator->getContext()->setScheme('');
$this->assertSame('//example.com/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
}
public function testAbsoluteUrlFallbackToPathIfSchemeAndHostAreEmpty()
{
$routes = $this->getRoutes('test', new Route('/path'));
$generator = $this->getGenerator($routes);
$generator->getContext()->setHost('');
$generator->getContext()->setScheme('');
$this->assertSame('/app.php/path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
}
public function testAbsoluteUrlWithNonHttpSchemeAndEmptyHost()
{
$routes = $this->getRoutes('test', new Route('/path', [], [], [], '', ['file']));
$generator = $this->getGenerator($routes);
$generator->getContext()->setBaseUrl('');
$generator->getContext()->setHost('');
$this->assertSame('file:///path', $generator->generate('test', [], UrlGeneratorInterface::ABSOLUTE_URL));
}
public function testGenerateNetworkPath()
{
$routes = $this->getRoutes('test', new Route('/{name}', [], [], [], '{locale}.example.com', ['http']));

View File

@ -205,10 +205,11 @@ class CsvEncoder implements EncoderInterface, DecoderInterface
if (\is_array($value)) {
$this->flatten($value, $result, $keySeparator, $parentKey.$key.$keySeparator, $escapeFormulas);
} else {
if ($escapeFormulas && \in_array(substr($value, 0, 1), $this->formulasStartCharacters, true)) {
if ($escapeFormulas && \in_array(substr((string) $value, 0, 1), $this->formulasStartCharacters, true)) {
$result[$parentKey.$key] = "\t".$value;
} else {
$result[$parentKey.$key] = $value;
// Ensures an actual value is used when dealing with true and false
$result[$parentKey.$key] = false === $value ? 0 : (true === $value ? 1 : $value);
}
}
}

View File

@ -29,6 +29,24 @@ class CsvEncoderTest extends TestCase
$this->encoder = new CsvEncoder();
}
public function testTrueFalseValues()
{
$data = [
'string' => 'foo',
'int' => 2,
'false' => false,
'true' => true,
];
// Check that true and false are appropriately handled
$this->assertEquals(<<<'CSV'
string,int,false,true
foo,2,0,1
CSV
, $this->encoder->encode($data, 'csv'));
}
public function testSupportEncoding()
{
$this->assertTrue($this->encoder->supportsEncoding('csv'));

View File

@ -121,7 +121,7 @@ class IssnValidator extends ConstraintValidator
for ($i = 0; $i < 7; ++$i) {
// Multiply the first digit by 8, the second by 7, etc.
$checkSum += (8 - $i) * $canonical[$i];
$checkSum += (8 - $i) * (int) $canonical[$i];
}
if (0 !== $checkSum % 11) {

View File

@ -84,7 +84,7 @@ class LuhnValidator extends ConstraintValidator
// ^ ^ ^ ^ ^
// = 1+8 + 4 + 6 + 1+6 + 2
for ($i = $length - 2; $i >= 0; $i -= 2) {
$checkSum += array_sum(str_split($value[$i] * 2));
$checkSum += array_sum(str_split((int) $value[$i] * 2));
}
if (0 === $checkSum || 0 !== $checkSum % 10) {

View File

@ -48,13 +48,8 @@ class Caster
*/
public static function castObject($obj, $class, $hasDebugInfo = false)
{
if ($hasDebugInfo) {
$a = $obj->__debugInfo();
} elseif ($obj instanceof \Closure) {
$a = [];
} else {
$a = (array) $obj;
}
$a = $obj instanceof \Closure ? [] : (array) $obj;
if ($obj instanceof \__PHP_Incomplete_Class) {
return $a;
}
@ -88,6 +83,17 @@ class Caster
}
}
if ($hasDebugInfo && \is_array($debugInfo = $obj->__debugInfo())) {
foreach ($debugInfo as $k => $v) {
if (!isset($k[0]) || "\0" !== $k[0]) {
$k = self::PREFIX_VIRTUAL.$k;
}
unset($a[$k]);
$a[$k] = $v;
}
}
return $a;
}

View File

@ -88,7 +88,7 @@ class DateCaster
if (self::PERIOD_LIMIT === $i) {
$now = new \DateTimeImmutable();
$dates[] = sprintf('%s more', ($end = $p->getEndDate())
? ceil(($end->format('U.u') - $d->format('U.u')) / ($now->add($p->getDateInterval())->format('U.u') - $now->format('U.u')))
? ceil(($end->format('U.u') - $d->format('U.u')) / ((int) $now->add($p->getDateInterval())->format('U.u') - (int) $now->format('U.u')))
: $p->recurrences - $i
);
break;