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:
commit
06c17cdaa9
@ -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));
|
||||
}
|
||||
|
@ -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 '_';
|
||||
}
|
||||
}
|
||||
|
@ -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)])));
|
||||
|
@ -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.');
|
||||
}
|
||||
}
|
||||
|
@ -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 ':';
|
||||
}
|
||||
}
|
||||
|
@ -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');
|
||||
}
|
||||
|
@ -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',
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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 = [])
|
||||
{
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
30
src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php
Normal file
30
src/Symfony/Component/Lock/Store/ExpiringStoreTrait.php
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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) {
|
||||
|
@ -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']));
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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'));
|
||||
|
@ -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) {
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
Reference in New Issue
Block a user