Merge branch '4.3' into 4.4

* 4.3:
  [Cache] fix memory leak when using PhpFilesAdapter
  [Yaml] Implement multiline string as scalar block for tagged values
  [HttpFoundation] Use `Cache-Control: must-revalidate` only if explicit lifetime has been given
  [FrameworkBundle] Use UserInterface to @return in getUser method
  [CI] Replace php7.4snapshot with php7.4 in Travis configuration
  [ExpressionLanguage][Node][BinaryNode] Process division by zero
  forward caught exception
  [Validator][ConstraintValidator] Stop passing unnecessary timezone argument to \DateTime
  add tags before processing them
  [MonologBridge] Fix debug processor datetime type
This commit is contained in:
Nicolas Grekas 2019-12-10 11:33:21 +01:00
commit e77059825c
18 changed files with 156 additions and 36 deletions

View File

@ -29,7 +29,7 @@ matrix:
env: php_extra="7.2 7.4snapshot"
- php: 7.3
env: deps=high
- php: 7.4snapshot
- php: 7.4
env: deps=low
fast_finish: true

View File

@ -33,7 +33,7 @@ class DebugProcessor implements DebugLoggerInterface, ResetInterface
$hash = $this->requestStack && ($request = $this->requestStack->getCurrentRequest()) ? spl_object_hash($request) : '';
$this->records[$hash][] = [
'timestamp' => $record['datetime']->getTimestamp(),
'timestamp' => $record['datetime'] instanceof \DateTimeInterface ? $record['datetime']->getTimestamp() : strtotime($record['datetime']),
'message' => $record['message'],
'priority' => $record['level'],
'priorityName' => $record['level_name'],

View File

@ -19,6 +19,30 @@ use Symfony\Component\HttpFoundation\RequestStack;
class DebugProcessorTest extends TestCase
{
/**
* @dataProvider providerDatetimeFormatTests
*/
public function testDatetimeFormat(array $record, $expectedTimestamp)
{
$processor = new DebugProcessor();
$processor($record);
$records = $processor->getLogs();
self::assertCount(1, $records);
self::assertSame($expectedTimestamp, $records[0]['timestamp']);
}
public function providerDatetimeFormatTests(): array
{
$record = $this->getRecord();
return [
[array_merge($record, ['datetime' => new \DateTime('2019-01-01T00:01:00+00:00')]), 1546300860],
[array_merge($record, ['datetime' => '2019-01-01T00:01:00+00:00']), 1546300860],
[array_merge($record, ['datetime' => 'foo']), false],
];
}
public function testDebugProcessor()
{
$processor = new DebugProcessor();
@ -75,7 +99,7 @@ class DebugProcessorTest extends TestCase
$debugProcessorChild->countErrors();
}
private function getRecord($level = Logger::WARNING, $message = 'test')
private function getRecord($level = Logger::WARNING, $message = 'test'): array
{
return [
'message' => $message,

View File

@ -30,6 +30,7 @@ use Symfony\Component\Messenger\Envelope;
use Symfony\Component\Messenger\Stamp\StampInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Security\Core\Exception\AccessDeniedException;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Csrf\CsrfToken;
use Symfony\Component\WebLink\EventListener\AddLinkHeaderListener;
use Symfony\Component\WebLink\GenericLinkProvider;
@ -352,7 +353,7 @@ trait ControllerTrait
/**
* Get a user from the Security Token Storage.
*
* @return object|null
* @return UserInterface|object|null
*
* @throws \LogicException If SecurityBundle is not available
*

View File

@ -32,7 +32,8 @@ class TwigBundle extends Bundle
{
parent::build($container);
$container->addCompilerPass(new ExtensionPass());
// ExtensionPass must be run before the FragmentRendererPass as it adds tags that are processed later
$container->addCompilerPass(new ExtensionPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 10);
$container->addCompilerPass(new TwigEnvironmentPass());
$container->addCompilerPass(new TwigLoaderPass());
$container->addCompilerPass(new ExceptionListenerPass());

View File

@ -0,0 +1,31 @@
<?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\Cache\Tests\Adapter;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Component\Cache\Adapter\PhpFilesAdapter;
/**
* @group time-sensitive
*/
class PhpFilesAdapterAppendOnlyTest extends PhpFilesAdapterTest
{
protected $skippedTests = [
'testDefaultLifeTime' => 'PhpFilesAdapter does not allow configuring a default lifetime.',
'testExpiration' => 'PhpFilesAdapter in append-only mode does not expiration.',
];
public function createCachePool(): CacheItemPoolInterface
{
return new PhpFilesAdapter('sf-cache', 0, null, true);
}
}

View File

@ -35,12 +35,13 @@ trait PhpFilesTrait
private $files = [];
private static $startTime;
private static $valuesCache = [];
public static function isSupported()
{
self::$startTime = self::$startTime ?? $_SERVER['REQUEST_TIME'] ?? time();
return \function_exists('opcache_invalidate') && ('cli' !== \PHP_SAPI || filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOLEAN)) && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN);
return \function_exists('opcache_invalidate') && filter_var(ini_get('opcache.enable'), FILTER_VALIDATE_BOOLEAN) && (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) || filter_var(ini_get('opcache.enable_cli'), FILTER_VALIDATE_BOOLEAN));
}
/**
@ -100,7 +101,6 @@ trait PhpFilesTrait
} elseif (!\is_object($value)) {
$values[$id] = $value;
} elseif (!$value instanceof LazyValue) {
// calling a Closure is for @deprecated BC and should be removed in Symfony 5.0
$values[$id] = $value();
} elseif (false === $values[$id] = include $value->file) {
unset($values[$id], $this->values[$id]);
@ -123,14 +123,20 @@ trait PhpFilesTrait
try {
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
if (\is_array($expiresAt = include $file)) {
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $this->values[$id]] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $this->values[$id]] = $expiresAt;
} elseif ($now < $expiresAt) {
$this->values[$id] = new LazyValue($file);
}
if ($now >= $expiresAt) {
unset($this->values[$id], $missingIds[$k]);
unset($this->values[$id], $missingIds[$k], self::$valuesCache[$file]);
}
} catch (\ErrorException $e) {
unset($missingIds[$k]);
@ -159,7 +165,13 @@ trait PhpFilesTrait
$file = $this->files[$id] ?? $this->files[$id] = $this->getFile($id);
$getExpiry = true;
if (\is_array($expiresAt = include $file)) {
if (isset(self::$valuesCache[$file])) {
[$expiresAt, $value] = self::$valuesCache[$file];
} elseif (\is_array($expiresAt = include $file)) {
if ($this->appendOnly) {
self::$valuesCache[$file] = $expiresAt;
}
[$expiresAt, $value] = $expiresAt;
} elseif ($this->appendOnly) {
$value = new LazyValue($file);
@ -213,12 +225,14 @@ trait PhpFilesTrait
$encodedKey = rawurlencode($key);
if (!$isStaticValue) {
if ($isStaticValue) {
$value = "return [{$expiry}, {$value}];";
} elseif ($this->appendOnly) {
$value = "return [{$expiry}, static function () { return {$value}; }];";
} else {
// We cannot use a closure here because of https://bugs.php.net/76982
$value = str_replace('\Symfony\Component\VarExporter\Internal\\', '', $value);
$value = "namespace Symfony\Component\VarExporter\Internal;\n\nreturn \$getExpiry ? {$expiry} : {$value};";
} else {
$value = "return [{$expiry}, {$value}];";
}
$file = $this->files[$key] = $this->getFile($key, true);
@ -229,6 +243,7 @@ trait PhpFilesTrait
@opcache_invalidate($file, true);
@opcache_compile_file($file);
}
unset(self::$valuesCache[$file]);
}
if (!$ok && !is_writable($this->directory)) {
@ -262,6 +277,8 @@ trait PhpFilesTrait
protected function doUnlink($file)
{
unset(self::$valuesCache[$file]);
if (self::isSupported()) {
@opcache_invalidate($file, true);
}

View File

@ -147,8 +147,16 @@ class BinaryNode extends Node
case '*':
return $left * $right;
case '/':
if (0 == $right) {
throw new \DivisionByZeroError('Division by zero');
}
return $left / $right;
case '%':
if (0 == $right) {
throw new \DivisionByZeroError('Modulo by zero');
}
return $left % $right;
case 'matches':
return preg_match($right, $left);

View File

@ -276,13 +276,13 @@ class ResponseHeaderBag extends HeaderBag
*/
protected function computeCacheControlValue()
{
if (!$this->cacheControl && !$this->has('ETag') && !$this->has('Last-Modified') && !$this->has('Expires')) {
return 'no-cache, private';
}
if (!$this->cacheControl) {
if ($this->has('Last-Modified') || $this->has('Expires')) {
return 'private, must-revalidate'; // allows for heuristic expiration (RFC 7234 Section 4.2.2) in the case of "Last-Modified"
}
// conservative by default
return 'private, must-revalidate';
return 'no-cache, private';
}
$header = $this->getCacheControlHeader();

View File

@ -51,9 +51,9 @@ class ResponseHeaderBagTest extends TestCase
$this->assertTrue($bag->hasCacheControlDirective('public'));
$bag = new ResponseHeaderBag(['ETag' => 'abcde']);
$this->assertEquals('private, must-revalidate', $bag->get('Cache-Control'));
$this->assertEquals('no-cache, private', $bag->get('Cache-Control'));
$this->assertTrue($bag->hasCacheControlDirective('private'));
$this->assertTrue($bag->hasCacheControlDirective('must-revalidate'));
$this->assertTrue($bag->hasCacheControlDirective('no-cache'));
$this->assertFalse($bag->hasCacheControlDirective('max-age'));
$bag = new ResponseHeaderBag(['Expires' => 'Wed, 16 Feb 2011 14:17:43 GMT']);

View File

@ -110,8 +110,6 @@ class ResponseCacheStrategy implements ResponseCacheStrategyInterface
$response->headers->set('Age', $this->age);
if ($this->isNotCacheableResponseEmbedded) {
$response->setExpires($response->getDate());
if ($this->flagDirectives['no-store']) {
$response->headers->set('Cache-Control', 'no-cache, no-store, must-revalidate');
} else {

View File

@ -1240,7 +1240,6 @@ class HttpCacheTest extends HttpCacheTestCase
$this->request('GET', '/', [], [], true);
$this->assertEquals('Hello World! My name is Bobby.', $this->response->getContent());
$this->assertNull($this->response->getTtl());
$this->assertTrue($this->response->mustRevalidate());
$this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
}
@ -1271,7 +1270,6 @@ class HttpCacheTest extends HttpCacheTestCase
// This can neither be cached nor revalidated, so it should be private/no cache
$this->assertEmpty($this->response->getContent());
$this->assertNull($this->response->getTtl());
$this->assertTrue($this->response->mustRevalidate());
$this->assertTrue($this->response->headers->hasCacheControlDirective('private'));
$this->assertTrue($this->response->headers->hasCacheControlDirective('no-cache'));
}

View File

@ -174,14 +174,14 @@ class PropertyAccessor implements PropertyAccessorInterface
$value = $zval[self::VALUE];
}
} catch (\TypeError $e) {
self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath);
self::throwInvalidArgumentException($e->getMessage(), $e->getTrace(), 0, $propertyPath, $e);
// It wasn't thrown in this class so rethrow it
throw $e;
}
}
private static function throwInvalidArgumentException(string $message, array $trace, int $i, string $propertyPath): void
private static function throwInvalidArgumentException(string $message, array $trace, int $i, string $propertyPath, \Throwable $previous = null): void
{
// the type mismatch is not caused by invalid arguments (but e.g. by an incompatible return type hint of the writer method)
if (0 !== strpos($message, 'Argument ')) {
@ -195,7 +195,7 @@ class PropertyAccessor implements PropertyAccessorInterface
$type = substr($message, 2 + $j, strpos($message, ' given', $j) - $j - 2);
$message = substr($message, $pos, $j - $pos);
throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $message, 'NULL' === $type ? 'null' : $type, $propertyPath));
throw new InvalidArgumentException(sprintf('Expected argument of type "%s", "%s" given at property path "%s".', $message, 'NULL' === $type ? 'null' : $type, $propertyPath), 0, $previous);
}
}

View File

@ -93,10 +93,7 @@ abstract class ConstraintValidator implements ConstraintValidatorInterface
// neither the native nor the stub IntlDateFormatter support
// DateTimeImmutable as of yet
if (!$value instanceof \DateTime) {
$value = new \DateTime(
$value->format('Y-m-d H:i:s.u e'),
$value->getTimezone()
);
$value = new \DateTime($value->format('Y-m-d H:i:s.u e'));
}
return $formatter->format($value);

View File

@ -53,10 +53,7 @@ abstract class AbstractComparisonValidatorTestCase extends ConstraintValidatorTe
foreach ($comparison as $i => $value) {
if ($value instanceof \DateTime) {
$comparison[$i] = new \DateTimeImmutable(
$value->format('Y-m-d H:i:s.u e'),
$value->getTimezone()
);
$comparison[$i] = new \DateTimeImmutable($value->format('Y-m-d H:i:s.u e'));
$add = true;
} elseif ('DateTime' === $value) {
$comparison[$i] = 'DateTimeImmutable';

View File

@ -70,7 +70,7 @@ class Dumper
$blockIndentationIndicator = (' ' === substr($value, 0, 1)) ? (string) $this->indentation : '';
$output .= sprintf("%s%s%s |%s\n", $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', '', $blockIndentationIndicator);
foreach (preg_split('/\n|\r\n/', $value) as $row) {
foreach (explode("\n", $value) as $row) {
$output .= sprintf("%s%s%s\n", $prefix, str_repeat(' ', $this->indentation), $row);
}
@ -80,6 +80,19 @@ class Dumper
if ($value instanceof TaggedValue) {
$output .= sprintf('%s%s !%s', $prefix, $dumpAsMap ? Inline::dump($key, $flags).':' : '-', $value->getTag());
if ($inline >= 1 && Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK & $flags && \is_string($value->getValue()) && false !== strpos($value->getValue(), "\n") && false === strpos($value->getValue(), "\r\n")) {
// If the first line starts with a space character, the spec requires a blockIndicationIndicator
// http://www.yaml.org/spec/1.2/spec.html#id2793979
$blockIndentationIndicator = (' ' === substr($value->getValue(), 0, 1)) ? (string) $this->indentation : '';
$output .= sprintf(" |%s\n", $blockIndentationIndicator);
foreach (explode("\n", $value->getValue()) as $row) {
$output .= sprintf("%s%s%s\n", $prefix, str_repeat(' ', $this->indentation), $row);
}
continue;
}
if ($inline - 1 <= 0 || null === $value->getValue() || is_scalar($value->getValue())) {
$output .= ' '.$this->dump($value->getValue(), $inline - 1, 0, $flags)."\n";
} else {

View File

@ -487,6 +487,39 @@ YAML;
$this->assertSame($expected, $this->dumper->dump($data, 2));
}
public function testDumpingMultiLineStringAsScalarBlockTaggedValue()
{
$data = [
'foo' => new TaggedValue('bar', "foo\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"),
];
$expected = <<<YAML
foo: !bar |
foo
line with trailing spaces:
bar
integer like line:
123456789
empty line:
baz
YAML;
$this->assertSame($expected, $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
}
public function testDumpingInlinedMultiLineIfRnBreakLineInTaggedValue()
{
$data = [
'data' => [
'foo' => new TaggedValue('bar', "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"),
],
];
$this->assertSame(file_get_contents(__DIR__.'/Fixtures/multiple_lines_as_literal_block_for_tagged_values.yml'), $this->dumper->dump($data, 2, 0, Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK));
}
public function testDumpMultiLineStringAsScalarBlock()
{
$data = [

View File

@ -0,0 +1,2 @@
data:
foo: !bar "foo\r\nline with trailing spaces:\n \nbar\ninteger like line:\n123456789\nempty line:\n\nbaz"