Merge branch '4.1' into 4.2

* 4.1:
  [Serializer] fixed DateTimeNormalizer to maintain microseconds when a different timezone required
  [Routing] fix taking verb into account when redirecting
  [DI] Fix dumping expressions accessing single-use private services
  [WebProfilerBundle] Split form field heading
This commit is contained in:
Nicolas Grekas 2018-12-02 14:25:28 +01:00
commit 2c00c917db
18 changed files with 148 additions and 32 deletions

View File

@ -177,6 +177,12 @@
color: inherit; color: inherit;
text-decoration: inherit; text-decoration: inherit;
} }
h2 + h3.form-data-type {
margin-top: 0;
}
h3.form-data-type + h3 {
margin-top: 1em;
}
</style> </style>
{% endblock %} {% endblock %}
@ -455,9 +461,10 @@
{% macro form_tree_details(name, data, forms_by_hash, show) %} {% macro form_tree_details(name, data, forms_by_hash, show) %}
{% import _self as tree %} {% import _self as tree %}
<div class="tree-details{% if not show|default(false) %} hidden{% endif %}" {% if data.id is defined %}id="{{ data.id }}-details"{% endif %}> <div class="tree-details{% if not show|default(false) %} hidden{% endif %}" {% if data.id is defined %}id="{{ data.id }}-details"{% endif %}>
<h2 class="dump-inline"> <h2>{{ name|default('(no name)') }}</h2>
{{ name|default('(no name)') }} {% if data.type_class is defined %}({{ profiler_dump(data.type_class) }}){% endif %} {% if data.type_class is defined %}
</h2> <h3 class="dump-inline form-data-type">{{ profiler_dump(data.type_class) }}</h3>
{% endif %}
{% if data.errors is defined and data.errors|length > 0 %} {% if data.errors is defined and data.errors|length > 0 %}
<div class="errors"> <div class="errors">

View File

@ -597,9 +597,7 @@ EOF;
if ($callable[0] instanceof Reference if ($callable[0] instanceof Reference
|| ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0])) || ($callable[0] instanceof Definition && $this->definitionVariables->contains($callable[0]))
) { ) {
$callable[0] = $this->dumpValue($callable[0]); return sprintf(" %s->%s(\$%s);\n", $this->dumpValue($callable[0]), $callable[1], $variableName);
return sprintf(' '.('$' === $callable[0][0] ? '%s' : '(%s)')."->%s(\$%s);\n", $callable[0], $callable[1], $variableName);
} }
$class = $this->dumpValue($callable[0]); $class = $this->dumpValue($callable[0]);
@ -1639,6 +1637,7 @@ EOF;
if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) { if ($definition->isShared() && !isset($this->singleUsePrivateIds[$id])) {
$code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code); $code = sprintf('$this->%s[\'%s\'] = %s', $definition->isPublic() ? 'services' : 'privates', $id, $code);
} }
$code = "($code)";
} elseif ($this->asFiles && !$this->isHotPath($definition)) { } elseif ($this->asFiles && !$this->isHotPath($definition)) {
$code = sprintf("\$this->load('%s.php')", $this->generateMethodName($id)); $code = sprintf("\$this->load('%s.php')", $this->generateMethodName($id));
if (!$definition->isShared()) { if (!$definition->isShared()) {

View File

@ -799,7 +799,7 @@ class PhpDumperTest extends TestCase
->setPublic(false); ->setPublic(false);
$container->register('public_foo', 'stdClass') $container->register('public_foo', 'stdClass')
->setPublic(true) ->setPublic(true)
->addArgument(new Expression('service("private_foo")')); ->addArgument(new Expression('service("private_foo").bar'));
$container->compile(); $container->compile();
$dumper = new PhpDumper($container); $dumper = new PhpDumper($container);

View File

@ -338,7 +338,7 @@ use Symfony\Component\DependencyInjection\Exception\RuntimeException;
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
yield 0 => ($this->services['foo'] ?? $this->load('getFooService.php')); yield 0 => ($this->services['foo'] ?? $this->load('getFooService.php'));
yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar()));
}, 2)); }, 2));
[Container%s/getThrowingOneService.php] => <?php [Container%s/getThrowingOneService.php] => <?php

View File

@ -391,7 +391,7 @@ class ProjectServiceContainer extends Container
{ {
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
yield 0 => ($this->services['foo'] ?? $this->getFooService()); yield 0 => ($this->services['foo'] ?? $this->getFooService());
yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar()));
}, 2)); }, 2));
} }

View File

@ -103,7 +103,7 @@ class Symfony_DI_PhpDumper_Test_Almost_Circular_Public extends Container
{ {
$this->services['bar3'] = $instance = new \BarCircular(); $this->services['bar3'] = $instance = new \BarCircular();
$a = ($this->services['foobar3'] ?? $this->services['foobar3'] = new \FoobarCircular()); $a = ($this->services['foobar3'] ?? ($this->services['foobar3'] = new \FoobarCircular()));
$instance->addFoobar($a, $a); $instance->addFoobar($a, $a);

View File

@ -59,7 +59,7 @@ class ProjectServiceContainer extends Container
*/ */
protected function getBarService() protected function getBarService()
{ {
return $this->services['bar'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? $this->privates['bar_%env(BAR)%'] = new \stdClass())); return $this->services['bar'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? ($this->privates['bar_%env(BAR)%'] = new \stdClass())));
} }
/** /**
@ -69,7 +69,7 @@ class ProjectServiceContainer extends Container
*/ */
protected function getFooService() protected function getFooService()
{ {
return $this->services['foo'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? $this->privates['bar_%env(BAR)%'] = new \stdClass()), array('baz_'.$this->getEnv('string:BAR') => new \stdClass())); return $this->services['foo'] = new \stdClass(($this->privates['bar_%env(BAR)%'] ?? ($this->privates['bar_%env(BAR)%'] = new \stdClass())), array('baz_'.$this->getEnv('string:BAR') => new \stdClass()));
} }
public function getParameter($name) public function getParameter($name)

View File

@ -391,7 +391,7 @@ class Symfony_DI_PhpDumper_Errored_Definition extends Container
{ {
return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () { return $this->services['tagged_iterator'] = new \Bar(new RewindableGenerator(function () {
yield 0 => ($this->services['foo'] ?? $this->getFooService()); yield 0 => ($this->services['foo'] ?? $this->getFooService());
yield 1 => ($this->privates['tagged_iterator_foo'] ?? $this->privates['tagged_iterator_foo'] = new \Bar()); yield 1 => ($this->privates['tagged_iterator_foo'] ?? ($this->privates['tagged_iterator_foo'] = new \Bar()));
}, 2)); }, 2));
} }

View File

@ -65,7 +65,7 @@ class ProjectServiceContainer extends Container
*/ */
protected function getBarServiceService() protected function getBarServiceService()
{ {
return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass())));
} }
/** /**
@ -78,7 +78,7 @@ class ProjectServiceContainer extends Container
return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('bar' => function () { return $this->services['foo_service'] = new \Symfony\Component\DependencyInjection\ServiceLocator(array('bar' => function () {
return ($this->services['bar_service'] ?? $this->getBarServiceService()); return ($this->services['bar_service'] ?? $this->getBarServiceService());
}, 'baz' => function (): \stdClass { }, 'baz' => function (): \stdClass {
return ($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass()); return ($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass()));
}, 'nil' => function () { }, 'nil' => function () {
return NULL; return NULL;
})); }));
@ -122,7 +122,7 @@ class ProjectServiceContainer extends Container
protected function getTranslator1Service() protected function getTranslator1Service()
{ {
return $this->services['translator_1'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_1' => function () { return $this->services['translator_1'] = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_1' => function () {
return ($this->services['translator.loader_1'] ?? $this->services['translator.loader_1'] = new \stdClass()); return ($this->services['translator.loader_1'] ?? ($this->services['translator.loader_1'] = new \stdClass()));
}))); })));
} }
@ -134,10 +134,10 @@ class ProjectServiceContainer extends Container
protected function getTranslator2Service() protected function getTranslator2Service()
{ {
$this->services['translator_2'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_2' => function () { $this->services['translator_2'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_2' => function () {
return ($this->services['translator.loader_2'] ?? $this->services['translator.loader_2'] = new \stdClass()); return ($this->services['translator.loader_2'] ?? ($this->services['translator.loader_2'] = new \stdClass()));
}))); })));
$instance->addResource('db', ($this->services['translator.loader_2'] ?? $this->services['translator.loader_2'] = new \stdClass()), 'nl'); $instance->addResource('db', ($this->services['translator.loader_2'] ?? ($this->services['translator.loader_2'] = new \stdClass())), 'nl');
return $instance; return $instance;
} }
@ -150,10 +150,10 @@ class ProjectServiceContainer extends Container
protected function getTranslator3Service() protected function getTranslator3Service()
{ {
$this->services['translator_3'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_3' => function () { $this->services['translator_3'] = $instance = new \Symfony\Component\DependencyInjection\Tests\Fixtures\StubbedTranslator(new \Symfony\Component\DependencyInjection\ServiceLocator(array('translator.loader_3' => function () {
return ($this->services['translator.loader_3'] ?? $this->services['translator.loader_3'] = new \stdClass()); return ($this->services['translator.loader_3'] ?? ($this->services['translator.loader_3'] = new \stdClass()));
}))); })));
$a = ($this->services['translator.loader_3'] ?? $this->services['translator.loader_3'] = new \stdClass()); $a = ($this->services['translator.loader_3'] ?? ($this->services['translator.loader_3'] = new \stdClass()));
$instance->addResource('db', $a, 'nl'); $instance->addResource('db', $a, 'nl');
$instance->addResource('db', $a, 'en'); $instance->addResource('db', $a, 'en');

View File

@ -56,7 +56,7 @@ class ProjectServiceContainer extends Container
*/ */
protected function getBarServiceService() protected function getBarServiceService()
{ {
return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); return $this->services['bar_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass())));
} }
/** /**
@ -66,6 +66,6 @@ class ProjectServiceContainer extends Container
*/ */
protected function getFooServiceService() protected function getFooServiceService()
{ {
return $this->services['foo_service'] = new \stdClass(($this->privates['baz_service'] ?? $this->privates['baz_service'] = new \stdClass())); return $this->services['foo_service'] = new \stdClass(($this->privates['baz_service'] ?? ($this->privates['baz_service'] = new \stdClass())));
} }
} }

View File

@ -56,6 +56,6 @@ class ProjectServiceContainer extends Container
*/ */
protected function getPublicFooService() protected function getPublicFooService()
{ {
return $this->services['public_foo'] = new \stdClass(new \stdClass()); return $this->services['public_foo'] = new \stdClass((new \stdClass())->bar);
} }
} }

View File

@ -96,7 +96,7 @@ class Symfony_DI_PhpDumper_Test_Uninitialized_Reference extends Container
{ {
$this->services['baz'] = $instance = new \stdClass(); $this->services['baz'] = $instance = new \stdClass();
$instance->foo3 = ($this->privates['foo3'] ?? $this->privates['foo3'] = new \stdClass()); $instance->foo3 = ($this->privates['foo3'] ?? ($this->privates['foo3'] = new \stdClass()));
return $instance; return $instance;
} }

View File

@ -67,7 +67,7 @@ trait PhpMatcherTrait
throw new ResourceNotFoundException(); throw new ResourceNotFoundException();
} }
private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): ?array private function doMatch(string $rawPathinfo, array &$allow = array(), array &$allowSchemes = array()): array
{ {
$allow = $allowSchemes = array(); $allow = $allowSchemes = array();
$pathinfo = rawurldecode($rawPathinfo) ?: '/'; $pathinfo = rawurldecode($rawPathinfo) ?: '/';
@ -91,7 +91,7 @@ trait PhpMatcherTrait
if ('/' === $pathinfo || $hasTrailingSlash === ('/' === $pathinfo[-1])) { if ('/' === $pathinfo || $hasTrailingSlash === ('/' === $pathinfo[-1])) {
// no-op // no-op
} elseif ($this instanceof RedirectableUrlMatcherInterface) { } elseif ($this instanceof RedirectableUrlMatcherInterface) {
return null; return $allow = $allowSchemes = array();
} else { } else {
continue; continue;
} }
@ -140,7 +140,7 @@ trait PhpMatcherTrait
} }
if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) {
if ($this instanceof RedirectableUrlMatcherInterface && (!$requiredMethods || isset($requiredMethods['GET']))) { if ($this instanceof RedirectableUrlMatcherInterface && (!$requiredMethods || isset($requiredMethods['GET']))) {
return null; return $allow = $allowSchemes = array();
} }
continue; continue;
} }
@ -176,6 +176,6 @@ trait PhpMatcherTrait
throw new NoConfigurationException(); throw new NoConfigurationException();
} }
return null; return array();
} }
} }

View File

@ -143,9 +143,9 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
} elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods))) { } elseif (!$supportsTrailingSlash || ($requiredMethods && !\in_array('GET', $requiredMethods))) {
continue; continue;
} elseif ('/' === $staticPrefix[-1] && substr($staticPrefix, 0, -1) === $pathinfo) { } elseif ('/' === $staticPrefix[-1] && substr($staticPrefix, 0, -1) === $pathinfo) {
return; return $this->allow = $this->allowSchemes = array();
} elseif ('/' === $pathinfo[-1] && substr($pathinfo, 0, -1) === $staticPrefix) { } elseif ('/' === $pathinfo[-1] && substr($pathinfo, 0, -1) === $staticPrefix) {
return; return $this->allow = $this->allowSchemes = array();
} else { } else {
continue; continue;
} }
@ -171,7 +171,7 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
} }
if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) { if ($hasTrailingSlash !== ('/' === $pathinfo[-1])) {
if (!$requiredMethods || \in_array('GET', $requiredMethods)) { if (!$requiredMethods || \in_array('GET', $requiredMethods)) {
return; return $this->allow = $this->allowSchemes = array();
} }
continue; continue;
} }
@ -212,6 +212,8 @@ class UrlMatcher implements UrlMatcherInterface, RequestMatcherInterface
return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array())); return $this->getAttributes($route, $name, array_replace($matches, $hostMatches, isset($status[1]) ? $status[1] : array()));
} }
return array();
} }
/** /**

View File

@ -170,6 +170,23 @@ class RedirectableUrlMatcherTest extends UrlMatcherTest
$matcher->match('/foo'); $matcher->match('/foo');
} }
public function testSlashAndVerbPrecedenceWithRedirection()
{
$coll = new RouteCollection();
$coll->add('a', new Route('/api/customers/{customerId}/contactpersons', array(), array(), array(), '', array(), array('post')));
$coll->add('b', new Route('/api/customers/{customerId}/contactpersons/', array(), array(), array(), '', array(), array('get')));
$matcher = $this->getUrlMatcher($coll);
$expected = array(
'_route' => 'b',
'customerId' => '123',
);
$this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons/'));
$matcher->expects($this->once())->method('redirect')->with('/api/customers/123/contactpersons/')->willReturn(array());
$this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons'));
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{ {
return $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($routes, $context ?: new RequestContext())); return $this->getMockForAbstractClass('Symfony\Component\Routing\Matcher\RedirectableUrlMatcher', array($routes, $context ?: new RequestContext()));

View File

@ -711,6 +711,20 @@ class UrlMatcherTest extends TestCase
$this->assertSame(array('_route' => 'b'), $matcher->match('/bar/')); $this->assertSame(array('_route' => 'b'), $matcher->match('/bar/'));
} }
public function testSlashAndVerbPrecedence()
{
$coll = new RouteCollection();
$coll->add('a', new Route('/api/customers/{customerId}/contactpersons/', array(), array(), array(), '', array(), array('post')));
$coll->add('b', new Route('/api/customers/{customerId}/contactpersons', array(), array(), array(), '', array(), array('get')));
$matcher = $this->getUrlMatcher($coll);
$expected = array(
'_route' => 'b',
'customerId' => '123',
);
$this->assertEquals($expected, $matcher->match('/api/customers/123/contactpersons'));
}
protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null) protected function getUrlMatcher(RouteCollection $routes, RequestContext $context = null)
{ {
return new UrlMatcher($routes, $context ?: new RequestContext()); return new UrlMatcher($routes, $context ?: new RequestContext());

View File

@ -68,7 +68,8 @@ class DateTimeNormalizer implements NormalizerInterface, DenormalizerInterface,
$timezone = $this->getTimezone($context); $timezone = $this->getTimezone($context);
if (null !== $timezone) { if (null !== $timezone) {
$object = (new \DateTimeImmutable('@'.$object->getTimestamp()))->setTimezone($timezone); $object = clone $object;
$object = $object->setTimezone($timezone);
} }
return $object->format($dateTimeFormat); return $object->format($dateTimeFormat);

View File

@ -103,6 +103,82 @@ class DateTimeNormalizerTest extends TestCase
yield array('2016-12-01T09:00:00+09:00', new \DateTimeImmutable('2016/12/01', new \DateTimeZone('UTC')), new \DateTimeZone('Japan')); yield array('2016-12-01T09:00:00+09:00', new \DateTimeImmutable('2016/12/01', new \DateTimeZone('UTC')), new \DateTimeZone('Japan'));
} }
/**
* @dataProvider normalizeUsingTimeZonePassedInContextAndExpectedFormatWithMicrosecondsProvider
*/
public function testNormalizeUsingTimeZonePassedInContextAndFormattedWithMicroseconds($expected, $expectedFormat, $input, $timezone)
{
$this->assertSame(
$expected,
$this->normalizer->normalize(
$input,
null,
array(
DateTimeNormalizer::TIMEZONE_KEY => $timezone,
DateTimeNormalizer::FORMAT_KEY => $expectedFormat,
)
)
);
}
public function normalizeUsingTimeZonePassedInContextAndExpectedFormatWithMicrosecondsProvider()
{
yield array(
'2018-12-01T18:03:06.067634',
'Y-m-d\TH:i:s.u',
\DateTime::createFromFormat(
'Y-m-d\TH:i:s.u',
'2018-12-01T18:03:06.067634',
new \DateTimeZone('UTC')
),
null,
);
yield array(
'2018-12-01T18:03:06.067634',
'Y-m-d\TH:i:s.u',
\DateTime::createFromFormat(
'Y-m-d\TH:i:s.u',
'2018-12-01T18:03:06.067634',
new \DateTimeZone('UTC')
),
new \DateTimeZone('UTC'),
);
yield array(
'2018-12-01T19:03:06.067634+01:00',
'Y-m-d\TH:i:s.uP',
\DateTimeImmutable::createFromFormat(
'Y-m-d\TH:i:s.u',
'2018-12-01T18:03:06.067634',
new \DateTimeZone('UTC')
),
new \DateTimeZone('Europe/Rome'),
);
yield array(
'2018-12-01T20:03:06.067634+02:00',
'Y-m-d\TH:i:s.uP',
\DateTime::createFromFormat(
'Y-m-d\TH:i:s.u',
'2018-12-01T18:03:06.067634',
new \DateTimeZone('UTC')
),
new \DateTimeZone('Europe/Kiev'),
);
yield array(
'2018-12-01T21:03:06.067634',
'Y-m-d\TH:i:s.u',
\DateTime::createFromFormat(
'Y-m-d\TH:i:s.u',
'2018-12-01T18:03:06.067634',
new \DateTimeZone('UTC')
),
new \DateTimeZone('Europe/Moscow'),
);
}
/** /**
* @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException * @expectedException \Symfony\Component\Serializer\Exception\InvalidArgumentException
* @expectedExceptionMessage The object must implement the "\DateTimeInterface". * @expectedExceptionMessage The object must implement the "\DateTimeInterface".