Merge branch '4.4'

* 4.4:
  [PhpUnitBridge] fix running simple-phpunit on Windows
  fixed UPGRADE
  fixed phpdocs
  fixed phpdocs
  [ErrorCatcher] Fixed some escaping in XML errors
  [Messenger] fix broken key normalization
  [FrameworkBundle] Allow creating chained cache pools by providing several adapters
  [FrameworkBundle] Use default_locale as default value for translator.fallbacks
This commit is contained in:
Nicolas Grekas 2019-07-05 08:37:37 +02:00
commit 22437d41bd
14 changed files with 179 additions and 22 deletions

View File

@ -339,7 +339,9 @@ Process
PropertyAccess PropertyAccess
-------------- --------------
* Removed support of passing `null` as 2nd argument of `PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0` instead. * Removed support of passing `null` as 2nd argument of
`PropertyAccessor::createCache()` method (`$defaultLifetime`), pass `0`
instead.
Routing Routing
------- -------

View File

@ -130,8 +130,9 @@ if (!file_exists("$PHPUNIT_DIR/phpunit-$PHPUNIT_VERSION/phpunit") || md5_file(__
} }
$prevRoot = getenv('COMPOSER_ROOT_VERSION'); $prevRoot = getenv('COMPOSER_ROOT_VERSION');
putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99"); putenv("COMPOSER_ROOT_VERSION=$PHPUNIT_VERSION.99");
$q = '\\' === DIRECTORY_SEPARATOR ? '"' : '';
// --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS // --no-suggest is not in the list to keep compat with composer 1.0, which is shipped with Ubuntu 16.04LTS
$exit = proc_close(proc_open("$COMPOSER install --no-dev --prefer-dist --no-progress --ansi", array(), $p, getcwd())); $exit = proc_close(proc_open("$q$COMPOSER install --no-dev --prefer-dist --no-progress --ansi$q", array(), $p, getcwd()));
putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : '')); putenv('COMPOSER_ROOT_VERSION'.(false !== $prevRoot ? '='.$prevRoot : ''));
if ($exit) { if ($exit) {
exit($exit); exit($exit);

View File

@ -25,6 +25,7 @@ CHANGELOG
* Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()` * Deprecated the `$parser` argument of `ControllerResolver::__construct()` and `DelegatingLoader::__construct()`
* Deprecated the `controller_name_converter` and `resolve_controller_name_subscriber` services * Deprecated the `controller_name_converter` and `resolve_controller_name_subscriber` services
* The `ControllerResolver` and `DelegatingLoader` classes have been marked as `final` * The `ControllerResolver` and `DelegatingLoader` classes have been marked as `final`
* Added support for configuring chained cache pools
4.3.0 4.3.0
----- -----

View File

@ -639,9 +639,10 @@ class Configuration implements ConfigurationInterface
->fixXmlConfig('path') ->fixXmlConfig('path')
->children() ->children()
->arrayNode('fallbacks') ->arrayNode('fallbacks')
->info('Defaults to the value of "default_locale".')
->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end() ->beforeNormalization()->ifString()->then(function ($v) { return [$v]; })->end()
->prototype('scalar')->end() ->prototype('scalar')->end()
->defaultValue(['en']) ->defaultValue([])
->end() ->end()
->booleanNode('logging')->defaultValue(false)->end() ->booleanNode('logging')->defaultValue(false)->end()
->scalarNode('formatter')->defaultValue('translator.formatter.default')->end() ->scalarNode('formatter')->defaultValue('translator.formatter.default')->end()
@ -846,8 +847,38 @@ class Configuration implements ConfigurationInterface
->arrayNode('pools') ->arrayNode('pools')
->useAttributeAsKey('name') ->useAttributeAsKey('name')
->prototype('array') ->prototype('array')
->fixXmlConfig('adapter')
->beforeNormalization()
->ifTrue(function ($v) { return (isset($v['adapters']) || \is_array($v['adapter'] ?? null)) && isset($v['provider']); })
->thenInvalid('Pool cannot have a "provider" while "adapter" is set to a map')
->end()
->children() ->children()
->scalarNode('adapter')->defaultValue('cache.app')->end() ->arrayNode('adapters')
->info('One or more adapters to chain for creating the pool, defaults to "cache.app".')
->beforeNormalization()
->always()->then(function ($values) {
if ([0] === array_keys($values) && \is_array($values[0])) {
return $values[0];
}
$adapters = [];
foreach ($values as $k => $v) {
if (\is_int($k) && \is_string($v)) {
$adapters[] = $v;
} elseif (!\is_array($v)) {
$adapters[$k] = $v;
} elseif (isset($v['provider'])) {
$adapters[$v['provider']] = $v['name'] ?? $v;
} else {
$adapters[] = $v['name'] ?? $v;
}
}
return $adapters;
})
->end()
->prototype('scalar')->end()
->end()
->scalarNode('tags')->defaultNull()->end() ->scalarNode('tags')->defaultNull()->end()
->booleanNode('public')->defaultFalse()->end() ->booleanNode('public')->defaultFalse()->end()
->integerNode('default_lifetime')->end() ->integerNode('default_lifetime')->end()
@ -967,6 +998,7 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->children() ->children()
->arrayNode('routing') ->arrayNode('routing')
->normalizeKeys(false)
->useAttributeAsKey('message_class') ->useAttributeAsKey('message_class')
->beforeNormalization() ->beforeNormalization()
->always() ->always()
@ -1026,6 +1058,7 @@ class Configuration implements ConfigurationInterface
->end() ->end()
->end() ->end()
->arrayNode('transports') ->arrayNode('transports')
->normalizeKeys(false)
->useAttributeAsKey('name') ->useAttributeAsKey('name')
->arrayPrototype() ->arrayPrototype()
->beforeNormalization() ->beforeNormalization()
@ -1073,6 +1106,7 @@ class Configuration implements ConfigurationInterface
->scalarNode('default_bus')->defaultNull()->end() ->scalarNode('default_bus')->defaultNull()->end()
->arrayNode('buses') ->arrayNode('buses')
->defaultValue(['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]]) ->defaultValue(['messenger.bus.default' => ['default_middleware' => true, 'middleware' => []]])
->normalizeKeys(false)
->useAttributeAsKey('name') ->useAttributeAsKey('name')
->arrayPrototype() ->arrayPrototype()
->addDefaultsIfNotSet() ->addDefaultsIfNotSet()

View File

@ -28,6 +28,7 @@ use Symfony\Component\BrowserKit\AbstractBrowser;
use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\TagAwareAdapter; use Symfony\Component\Cache\Adapter\TagAwareAdapter;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass; use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Cache\Marshaller\DefaultMarshaller; use Symfony\Component\Cache\Marshaller\DefaultMarshaller;
@ -278,7 +279,7 @@ class FrameworkExtension extends Extension
$this->registerEsiConfiguration($config['esi'], $container, $loader); $this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerSsiConfiguration($config['ssi'], $container, $loader); $this->registerSsiConfiguration($config['ssi'], $container, $loader);
$this->registerFragmentsConfiguration($config['fragments'], $container, $loader); $this->registerFragmentsConfiguration($config['fragments'], $container, $loader);
$this->registerTranslatorConfiguration($config['translator'], $container, $loader); $this->registerTranslatorConfiguration($config['translator'], $container, $loader, $config['default_locale']);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader); $this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerCacheConfiguration($config['cache'], $container); $this->registerCacheConfiguration($config['cache'], $container);
$this->registerWorkflowConfiguration($config['workflows'], $container, $loader); $this->registerWorkflowConfiguration($config['workflows'], $container, $loader);
@ -952,7 +953,7 @@ class FrameworkExtension extends Extension
return new Reference('assets.empty_version_strategy'); return new Reference('assets.empty_version_strategy');
} }
private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader) private function registerTranslatorConfiguration(array $config, ContainerBuilder $container, LoaderInterface $loader, string $defaultLocale)
{ {
if (!$this->isConfigEnabled($container, $config)) { if (!$this->isConfigEnabled($container, $config)) {
$container->removeDefinition('console.command.translation_debug'); $container->removeDefinition('console.command.translation_debug');
@ -967,7 +968,7 @@ class FrameworkExtension extends Extension
$container->setAlias('translator', 'translator.default')->setPublic(true); $container->setAlias('translator', 'translator.default')->setPublic(true);
$container->setAlias('translator.formatter', new Alias($config['formatter'], false)); $container->setAlias('translator.formatter', new Alias($config['formatter'], false));
$translator = $container->findDefinition('translator.default'); $translator = $container->findDefinition('translator.default');
$translator->addMethodCall('setFallbackLocales', [$config['fallbacks']]); $translator->addMethodCall('setFallbackLocales', [$config['fallbacks'] ?: [$defaultLocale]]);
$container->setParameter('translator.logging', $config['logging']); $container->setParameter('translator.logging', $config['logging']);
$container->setParameter('translator.default_path', $config['default_path']); $container->setParameter('translator.default_path', $config['default_path']);
@ -1650,16 +1651,29 @@ class FrameworkExtension extends Extension
} }
foreach (['app', 'system'] as $name) { foreach (['app', 'system'] as $name) {
$config['pools']['cache.'.$name] = [ $config['pools']['cache.'.$name] = [
'adapter' => $config[$name], 'adapters' => [$config[$name]],
'public' => true, 'public' => true,
'tags' => false, 'tags' => false,
]; ];
} }
foreach ($config['pools'] as $name => $pool) { foreach ($config['pools'] as $name => $pool) {
if ($config['pools'][$pool['adapter']]['tags'] ?? false) { $pool['adapters'] = $pool['adapters'] ?: ['cache.app'];
$pool['adapter'] = '.'.$pool['adapter'].'.inner';
foreach ($pool['adapters'] as $provider => $adapter) {
if ($config['pools'][$adapter]['tags'] ?? false) {
$pool['adapters'][$provider] = $adapter = '.'.$adapter.'.inner';
}
}
if (1 === \count($pool['adapters'])) {
if (!isset($pool['provider']) && !\is_int($provider)) {
$pool['provider'] = $provider;
}
$definition = new ChildDefinition($adapter);
} else {
$definition = new Definition(ChainAdapter::class, [$pool['adapters'], 0]);
$pool['reset'] = 'reset';
} }
$definition = new ChildDefinition($pool['adapter']);
if ($pool['tags']) { if ($pool['tags']) {
if (true !== $pool['tags'] && ($config['pools'][$pool['tags']]['tags'] ?? false)) { if (true !== $pool['tags'] && ($config['pools'][$pool['tags']]['tags'] ?? false)) {
@ -1690,7 +1704,7 @@ class FrameworkExtension extends Extension
} }
$definition->setPublic($pool['public']); $definition->setPublic($pool['public']);
unset($pool['adapter'], $pool['public'], $pool['tags']); unset($pool['adapters'], $pool['public'], $pool['tags']);
$definition->addTag('cache.pool', $pool); $definition->addTag('cache.pool', $pool);
$container->setDefinition($name, $definition); $container->setDefinition($name, $definition);

View File

@ -256,6 +256,10 @@
</xsd:complexType> </xsd:complexType>
<xsd:complexType name="cache_pool"> <xsd:complexType name="cache_pool">
<xsd:sequence>
<xsd:element name="adapter" type="cache_pool_adapter" minOccurs="0" maxOccurs="unbounded" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" /> <xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="adapter" type="xsd:string" /> <xsd:attribute name="adapter" type="xsd:string" />
<xsd:attribute name="tags" type="xsd:string" /> <xsd:attribute name="tags" type="xsd:string" />
@ -265,6 +269,11 @@
<xsd:attribute name="clearer" type="xsd:string" /> <xsd:attribute name="clearer" type="xsd:string" />
</xsd:complexType> </xsd:complexType>
<xsd:complexType name="cache_pool_adapter">
<xsd:attribute name="name" type="xsd:string" use="required" />
<xsd:attribute name="provider" type="xsd:string" />
</xsd:complexType>
<xsd:complexType name="workflow"> <xsd:complexType name="workflow">
<xsd:sequence> <xsd:sequence>
<xsd:element name="initial-marking" type="xsd:string" minOccurs="0" maxOccurs="unbounded" /> <xsd:element name="initial-marking" type="xsd:string" minOccurs="0" maxOccurs="unbounded" />

View File

@ -232,7 +232,7 @@ class ConfigurationTest extends TestCase
], ],
'translator' => [ 'translator' => [
'enabled' => !class_exists(FullStack::class), 'enabled' => !class_exists(FullStack::class),
'fallbacks' => ['en'], 'fallbacks' => [],
'logging' => false, 'logging' => false,
'formatter' => 'translator.formatter.default', 'formatter' => 'translator.formatter.default',
'paths' => [], 'paths' => [],

View File

@ -24,6 +24,14 @@ $container->loadFromExtension('framework', [
'cache.def' => [ 'cache.def' => [
'default_lifetime' => 11, 'default_lifetime' => 11,
], ],
'cache.chain' => [
'default_lifetime' => 12,
'adapter' => [
'cache.adapter.array',
'cache.adapter.filesystem',
'redis://foo' => 'cache.adapter.redis',
],
],
], ],
], ],
]); ]);

View File

@ -12,6 +12,11 @@
<framework:pool name="cache.baz" adapter="cache.adapter.filesystem" default-lifetime="7" /> <framework:pool name="cache.baz" adapter="cache.adapter.filesystem" default-lifetime="7" />
<framework:pool name="cache.foobar" adapter="cache.adapter.psr6" default-lifetime="10" provider="app.cache_pool" /> <framework:pool name="cache.foobar" adapter="cache.adapter.psr6" default-lifetime="10" provider="app.cache_pool" />
<framework:pool name="cache.def" default-lifetime="11" /> <framework:pool name="cache.def" default-lifetime="11" />
<framework:pool name="cache.chain" default-lifetime="12">
<framework:adapter name="cache.adapter.array" />
<framework:adapter name="cache.adapter.filesystem" />
<framework:adapter name="cache.adapter.redis" provider="redis://foo" />
</framework:pool>
</framework:cache> </framework:cache>
</framework:config> </framework:config>
</container> </container>

View File

@ -17,3 +17,9 @@ framework:
provider: app.cache_pool provider: app.cache_pool
cache.def: cache.def:
default_lifetime: 11 default_lifetime: 11
cache.chain:
default_lifetime: 12
adapter:
- cache.adapter.array
- cache.adapter.filesystem
- {name: cache.adapter.redis, provider: 'redis://foo'}

View File

@ -20,10 +20,12 @@ use Symfony\Bundle\FullStack;
use Symfony\Component\Cache\Adapter\AdapterInterface; use Symfony\Component\Cache\Adapter\AdapterInterface;
use Symfony\Component\Cache\Adapter\ApcuAdapter; use Symfony\Component\Cache\Adapter\ApcuAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\Cache\Adapter\DoctrineAdapter; use Symfony\Component\Cache\Adapter\DoctrineAdapter;
use Symfony\Component\Cache\Adapter\FilesystemAdapter; use Symfony\Component\Cache\Adapter\FilesystemAdapter;
use Symfony\Component\Cache\Adapter\ProxyAdapter; use Symfony\Component\Cache\Adapter\ProxyAdapter;
use Symfony\Component\Cache\Adapter\RedisAdapter; use Symfony\Component\Cache\Adapter\RedisAdapter;
use Symfony\Component\Cache\DependencyInjection\CachePoolPass;
use Symfony\Component\Config\Resource\DirectoryResource; use Symfony\Component\Config\Resource\DirectoryResource;
use Symfony\Component\Config\Resource\FileExistenceResource; use Symfony\Component\Config\Resource\FileExistenceResource;
use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ChildDefinition;
@ -1219,13 +1221,36 @@ abstract class FrameworkExtensionTest extends TestCase
public function testCachePoolServices() public function testCachePoolServices()
{ {
$container = $this->createContainerFromFile('cache'); $container = $this->createContainerFromFile('cache', [], true, false);
$container->setParameter('cache.prefix.seed', 'test');
$container->addCompilerPass(new CachePoolPass());
$container->compile();
$this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foo', 'cache.adapter.apcu', 30); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foo', 'cache.adapter.apcu', 30);
$this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.bar', 'cache.adapter.doctrine', 5); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.bar', 'cache.adapter.doctrine', 5);
$this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.baz', 'cache.adapter.filesystem', 7); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.baz', 'cache.adapter.filesystem', 7);
$this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foobar', 'cache.adapter.psr6', 10); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.foobar', 'cache.adapter.psr6', 10);
$this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.def', 'cache.app', 11); $this->assertCachePoolServiceDefinitionIsCreated($container, 'cache.def', 'cache.app', 11);
$chain = $container->getDefinition('cache.chain');
$this->assertSame(ChainAdapter::class, $chain->getClass());
$expected = [
[
(new ChildDefinition('cache.adapter.array'))
->replaceArgument(0, 12),
(new ChildDefinition('cache.adapter.filesystem'))
->replaceArgument(0, 'x5nX4TVTWn')
->replaceArgument(1, 12),
(new ChildDefinition('cache.adapter.redis'))
->replaceArgument(0, new Reference('.cache_connection.kYdiLgf'))
->replaceArgument(1, 'x5nX4TVTWn')
->replaceArgument(2, 12),
],
12,
];
$this->assertEquals($expected, $chain->getArguments());
} }
public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebug() public function testRemovesResourceCheckerConfigCacheFactoryArgumentOnlyIfNoDebug()

View File

@ -13,6 +13,7 @@ namespace Symfony\Component\Cache\DependencyInjection;
use Symfony\Component\Cache\Adapter\AbstractAdapter; use Symfony\Component\Cache\Adapter\AbstractAdapter;
use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Adapter\ChainAdapter;
use Symfony\Component\DependencyInjection\ChildDefinition; use Symfony\Component\DependencyInjection\ChildDefinition;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface; use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -97,7 +98,50 @@ class CachePoolPass implements CompilerPassInterface
if (isset($tags[0]['provider'])) { if (isset($tags[0]['provider'])) {
$tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider'])); $tags[0]['provider'] = new Reference(static::getServiceProvider($container, $tags[0]['provider']));
} }
$i = 0;
if (ChainAdapter::class === $class) {
$adapters = [];
foreach ($adapter->getArgument(0) as $provider => $adapter) {
$chainedPool = $adapter = new ChildDefinition($adapter);
$chainedTags = [\is_int($provider) ? [] : ['provider' => $provider]];
$chainedClass = '';
while ($adapter instanceof ChildDefinition) {
$adapter = $container->findDefinition($adapter->getParent());
$chainedClass = $chainedClass ?: $adapter->getClass();
if ($t = $adapter->getTag($this->cachePoolTag)) {
$chainedTags[0] += $t[0];
}
}
if (ChainAdapter::class === $chainedClass) {
throw new InvalidArgumentException(sprintf('Invalid service "%s": chain of adapters cannot reference another chain, found "%s".', $id, $chainedPool->getParent()));
}
$i = 0;
if (isset($chainedTags[0]['provider'])) {
$chainedPool->replaceArgument($i++, new Reference(static::getServiceProvider($container, $chainedTags[0]['provider'])));
}
if (isset($tags[0]['namespace']) && ArrayAdapter::class !== $adapter->getClass()) {
$chainedPool->replaceArgument($i++, $tags[0]['namespace']);
}
if (isset($tags[0]['default_lifetime'])) {
$chainedPool->replaceArgument($i++, $tags[0]['default_lifetime']);
}
$adapters[] = $chainedPool;
}
$pool->replaceArgument(0, $adapters);
unset($tags[0]['provider'], $tags[0]['namespace']);
$i = 1;
} else {
$i = 0;
}
foreach ($attributes as $attr) { foreach ($attributes as $attr) {
if (!isset($tags[0][$attr])) { if (!isset($tags[0][$attr])) {
// no-op // no-op
@ -105,7 +149,7 @@ class CachePoolPass implements CompilerPassInterface
if ($tags[0][$attr]) { if ($tags[0][$attr]) {
$pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]); $pool->addTag($this->kernelResetTag, ['method' => $tags[0][$attr]]);
} }
} elseif ('namespace' !== $attr || ArrayAdapter::class !== $adapter->getClass()) { } elseif ('namespace' !== $attr || ArrayAdapter::class !== $class) {
$pool->replaceArgument($i++, $tags[0][$attr]); $pool->replaceArgument($i++, $tags[0][$attr]);
} }
unset($tags[0][$attr]); unset($tags[0][$attr]);

View File

@ -55,14 +55,16 @@ class HtmlErrorRenderer implements ErrorRendererInterface
{ {
$css = $this->getStylesheet(); $css = $this->getStylesheet();
$body = $this->getBody($exception); $body = $this->getBody($exception);
$charset = $this->escapeHtml($this->charset);
$title = $this->escapeHtml($exception->getTitle());
return <<<EOF return <<<EOF
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head> <head>
<meta charset="{$this->charset}" /> <meta charset="{$charset}" />
<meta name="robots" content="noindex,nofollow,noarchive" /> <meta name="robots" content="noindex,nofollow,noarchive" />
<title>{$exception->getTitle()}</title> <title>{$title}</title>
<style>$css</style> <style>$css</style>
</head> </head>
<body> <body>
@ -94,11 +96,14 @@ EOF;
*/ */
public function getBody(FlattenException $exception) public function getBody(FlattenException $exception)
{ {
$statusCode = $this->escapeHtml($exception->getStatusCode());
$title = $this->escapeHtml($exception->getTitle());
if (!$this->debug) { if (!$this->debug) {
return <<<EOF return <<<EOF
<div class="container"> <div class="container">
<h1>Oops! An Error Occurred</h1> <h1>Oops! An Error Occurred</h1>
<h2>The server returned a "{$exception->getStatusCode()} {$exception->getTitle()}".</h2> <h2>The server returned a "{$statusCode} {$title}".</h2>
<p> <p>
Something is broken. Please let us know what you were doing when this error occurred. Something is broken. Please let us know what you were doing when this error occurred.
We will fix it as soon as possible. Sorry for any inconvenience caused. We will fix it as soon as possible. Sorry for any inconvenience caused.

View File

@ -40,7 +40,10 @@ class XmlErrorRenderer implements ErrorRendererInterface
*/ */
public function render(FlattenException $exception): string public function render(FlattenException $exception): string
{ {
$title = $this->escapeXml($exception->getTitle());
$message = $this->escapeXml($exception->getMessage()); $message = $this->escapeXml($exception->getMessage());
$statusCode = $this->escapeXml($exception->getStatusCode());
$charset = $this->escapeXml($this->charset);
$exceptions = ''; $exceptions = '';
if ($this->debug) { if ($this->debug) {
@ -63,10 +66,10 @@ class XmlErrorRenderer implements ErrorRendererInterface
} }
return <<<EOF return <<<EOF
<?xml version="1.0" encoding="{$this->charset}" ?> <?xml version="1.0" encoding="{$charset}" ?>
<problem xmlns="urn:ietf:rfc:7807"> <problem xmlns="urn:ietf:rfc:7807">
<title>{$exception->getTitle()}</title> <title>{$title}</title>
<status>{$exception->getStatusCode()}</status> <status>{$statusCode}</status>
<detail>{$message}</detail> <detail>{$message}</detail>
{$exceptions} {$exceptions}
</problem> </problem>