merged branch vicb/fmwk/config (PR #6852)

This PR was squashed before being merged into the master branch (closes #6852).

Commits
-------

fde7585 [DIC] Better handling of enableable configurations

Discussion
----------

[DIC] Better handling of enableable configurations

| Q             | A
| ------------- | ---
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no, this feature has not been released yet
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

My definition of bug fix might be discussable. The thing which I think is not discussable is that this PR fixes the semantic - and I think it is important for a "semantic configuration": before this PR, some nodes had `->canBeDisabled` for nodes that were actually disabled by default. Those nodes now have `->canBeEnabled` which sounds right.

**Edit: Jan 28, 2013** - history:

See [the related comments](https://github.com/symfony/symfony/pull/6829#discussion_r2727742).

I think Symfony **must** get the configuration right as we can expect of lot of devs to use this as a template when writting their own configuration.

@schmittjoh could you please give me your feedback on [this change](https://github.com/symfony/symfony/pull/6852/files#L4R224) considering [the rationale](https://github.com/symfony/symfony/pull/6852/files#L3R7).

---------------------------------------------------------------------------

by stof at 2013-01-23T16:10:33Z

@vicb your links are broken as they are pointing to the PR creation page

---------------------------------------------------------------------------

by stof at 2013-01-23T16:10:55Z

and to create a TODO list, it has to be a list first

---------------------------------------------------------------------------

by vicb at 2013-01-23T16:31:10Z

@stof thanks for reporting the broken links, they are fixed /cc @schmittjoh

---------------------------------------------------------------------------

by vicb at 2013-01-23T16:31:50Z

@Tobion please submit a PR to my repo, I don't have much time to work on this. Thanks !

---------------------------------------------------------------------------

by vicb at 2013-01-25T15:14:47Z

@fabpot @schmittjoh I'd like your feedback on the latest commit, rationale is in the method phpDoc. It better matches what we do now and seem the most sensible thing to do.

edit: with this you can no more disable the node explicitly, I have to find a better solution

---------------------------------------------------------------------------

by schmittjoh at 2013-01-25T15:20:13Z

Looks good.

On Fri, Jan 25, 2013 at 4:15 PM, Victor Berchet <notifications@github.com>wrote:

> @fabpot <https://github.com/fabpot> @schmittjoh<https://github.com/schmittjoh>I'd like your feedback on the latest commit, rationale is in the method
> phpDoc. It better matches what we do now and seem the most sensible thing
> to do.
>
> —
> Reply to this email directly or view it on GitHub<https://github.com/symfony/symfony/pull/6852#issuecomment-12704585>.
>
>

---------------------------------------------------------------------------

by vicb at 2013-01-28T14:37:57Z

@fabpot I know I keep insisting on this one and I am sorry for that but I think this should be considered as a bug fix (see the PR header for details) and should be merged in 2.2. I think the Symfony core should be exemplary as it is used by many developers as a template when creating their own bundle. *This PR is no more a WIP and can be merged right now*.

In addition to fixing the enableable nodes, this PR contain new UTs and some fixes to the code / tests.

---------------------------------------------------------------------------

by fabpot at 2013-01-28T16:43:42Z

@vicb As explained in a comment, this is not a BC break as this feature does not exist in 2.1. So, I can make the change to the CHANGELOG if you want after merging, or I can let you make the change.

---------------------------------------------------------------------------

by vicb at 2013-01-28T16:46:33Z

I am going to change it right now !

---------------------------------------------------------------------------

by vicb at 2013-01-28T16:46:56Z

(and thanks for having checked this)

---------------------------------------------------------------------------

by vicb at 2013-01-28T16:54:37Z

@fabpot I have updated the changelog and the PR header.

I am not sure if the commits should be squashed or not. On one side the multiple commits can help understand the changes but on the other side that's a lot of small commits which could pollute history. I let you choose what to do.
This commit is contained in:
Fabien Potencier 2013-01-28 18:21:49 +01:00
commit c1037b1603
12 changed files with 362 additions and 147 deletions

View File

@ -91,10 +91,10 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('form')
->info('form configuration')
->canBeDisabled()
->canBeEnabled()
->end()
->arrayNode('csrf_protection')
->canBeDisabled()
->canBeEnabled()
->children()
->scalarNode('field_name')->defaultValue('_token')->end()
->end()
@ -109,7 +109,7 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('esi')
->info('esi configuration')
->canBeDisabled()
->canBeEnabled()
->end()
->end()
;
@ -121,7 +121,7 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('router_proxy')
->info('proxy configuration for the HTTP content renderer')
->canBeDisabled()
->canBeEnabled()
->children()
->scalarNode('path')->defaultValue('/_proxy')->end()
->end()
@ -136,7 +136,7 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('profiler')
->info('profiler configuration')
->canBeDisabled()
->canBeEnabled()
->children()
->booleanNode('only_exceptions')->defaultFalse()->end()
->booleanNode('only_master_requests')->defaultFalse()->end()
@ -176,8 +176,10 @@ class Configuration implements ConfigurationInterface
->scalarNode('https_port')->defaultValue(443)->end()
->scalarNode('strict_requirements')
->info(
'set to false to disable exceptions when a route is '.
'generated with invalid parameters (and return null instead)'
"set to true to throw an exception when a parameter does not match the requirements\n".
"set to false to disable exceptions when a parameter does not match the requirements (and return null instead)\n".
"set to null to disable parameter checks against requirements\n".
"'true' is the preferred configuration in development mode, while 'false' or 'null' might be preferred in production"
)
->defaultTrue()
->end()
@ -367,7 +369,7 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('translator')
->info('translator configuration')
->canBeDisabled()
->canBeEnabled()
->children()
->scalarNode('fallback')->defaultValue('en')->end()
->end()
@ -382,7 +384,7 @@ class Configuration implements ConfigurationInterface
->children()
->arrayNode('validation')
->info('validation configuration')
->canBeDisabled()
->canBeEnabled()
->children()
->scalarNode('cache')->end()
->booleanNode('enable_annotations')->defaultFalse()->end()

View File

@ -81,37 +81,23 @@ class FrameworkExtension extends Extension
$this->registerSessionConfiguration($config['session'], $container, $loader);
}
if (isset($config['form']) && !empty($config['form']['enabled'])) {
if ($this->isConfigEnabled($container, $config['form'])) {
$this->registerFormConfiguration($config, $container, $loader);
$config['validation']['enabled'] = true;
}
if (!empty($config['validation']['enabled'])) {
$this->registerValidationConfiguration($config['validation'], $container, $loader);
}
if (isset($config['esi'])) {
$this->registerEsiConfiguration($config['esi'], $loader);
}
if (isset($config['router_proxy'])) {
$this->registerRouterProxyConfiguration($config['router_proxy'], $container, $loader);
}
if (isset($config['profiler'])) {
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
}
if (isset($config['router'])) {
$this->registerRouterConfiguration($config['router'], $container, $loader);
}
if (isset($config['templating'])) {
$this->registerTemplatingConfiguration($config['templating'], $config['ide'], $container, $loader);
}
if (isset($config['translator'])) {
$this->registerTranslatorConfiguration($config['translator'], $container);
$this->registerValidationConfiguration($config['validation'], $container, $loader);
$this->registerEsiConfiguration($config['esi'], $container, $loader);
$this->registerRouterProxyConfiguration($config['router_proxy'], $container, $loader);
$this->registerProfilerConfiguration($config['profiler'], $container, $loader);
$this->registerTranslatorConfiguration($config['translator'], $container);
if (isset($config['router'])) {
$this->registerRouterConfiguration($config['router'], $container, $loader);
}
$this->registerAnnotationsConfiguration($config['annotations'], $container, $loader);
@ -161,7 +147,7 @@ class FrameworkExtension extends Extension
private function registerFormConfiguration($config, ContainerBuilder $container, XmlFileLoader $loader)
{
$loader->load('form.xml');
if (isset($config['csrf_protection'])) {
if ($this->isConfigEnabled($container, $config['csrf_protection'])) {
if (!isset($config['session'])) {
throw new \LogicException('CSRF protection needs that sessions are enabled.');
}
@ -170,36 +156,44 @@ class FrameworkExtension extends Extension
}
$loader->load('form_csrf.xml');
$container->setParameter('form.type_extension.csrf.enabled', $config['csrf_protection']['enabled']);
$container->setParameter('form.type_extension.csrf.enabled', true);
$container->setParameter('form.type_extension.csrf.field_name', $config['csrf_protection']['field_name']);
} else {
$container->setParameter('form.type_extension.csrf.enabled', false);
}
}
/**
* Loads the ESI configuration.
*
* @param array $config An ESI configuration array
* @param XmlFileLoader $loader An XmlFileLoader instance
* @param array $config A proxy configuration array
* @param ContainerBuilder $container A ContainerBuilder instance
* @param XmlFileLoader $loader An XmlFileLoader instance
*/
private function registerEsiConfiguration(array $config, XmlFileLoader $loader)
private function registerEsiConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!empty($config['enabled'])) {
$loader->load('esi.xml');
if (!$this->isConfigEnabled($container, $config)) {
return;
}
$loader->load('esi.xml');
}
/**
* Loads the router proxy configuration.
*
* @param array $config A proxy configuration array
* @param XmlFileLoader $loader An XmlFileLoader instance
* @param array $config A proxy configuration array
* @param ContainerBuilder $container A ContainerBuilder instance
* @param XmlFileLoader $loader An XmlFileLoader instance
*/
private function registerRouterProxyConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!empty($config['enabled'])) {
$loader->load('proxy.xml');
$container->setParameter('http_content_renderer.proxy_path', $config['path']);
if (!$this->isConfigEnabled($container, $config)) {
return;
}
$loader->load('proxy.xml');
$container->setParameter('http_content_renderer.proxy_path', $config['path']);
}
/**
@ -258,7 +252,7 @@ class FrameworkExtension extends Extension
}
}
if (!$config['enabled']) {
if (!$this->isConfigEnabled($container, $config)) {
$container->getDefinition('profiler')->addMethodCall('disable', array());
}
}
@ -530,61 +524,63 @@ class FrameworkExtension extends Extension
*/
private function registerTranslatorConfiguration(array $config, ContainerBuilder $container)
{
if (!empty($config['enabled'])) {
// Use the "real" translator instead of the identity default
$container->setAlias('translator', 'translator.default');
$translator = $container->findDefinition('translator.default');
$translator->addMethodCall('setFallbackLocale', array($config['fallback']));
if (!$this->isConfigEnabled($container, $config)) {
return;
}
// Discover translation directories
$dirs = array();
if (class_exists('Symfony\Component\Validator\Validator')) {
$r = new \ReflectionClass('Symfony\Component\Validator\Validator');
// Use the "real" translator instead of the identity default
$container->setAlias('translator', 'translator.default');
$translator = $container->findDefinition('translator.default');
$translator->addMethodCall('setFallbackLocale', array($config['fallback']));
$dirs[] = dirname($r->getFilename()).'/Resources/translations';
}
if (class_exists('Symfony\Component\Form\Form')) {
$r = new \ReflectionClass('Symfony\Component\Form\Form');
// Discover translation directories
$dirs = array();
if (class_exists('Symfony\Component\Validator\Validator')) {
$r = new \ReflectionClass('Symfony\Component\Validator\Validator');
$dirs[] = dirname($r->getFilename()).'/Resources/translations';
}
if (class_exists('Symfony\Component\Security\Core\Exception\AuthenticationException')) {
$r = new \ReflectionClass('Symfony\Component\Security\Core\Exception\AuthenticationException');
$dirs[] = dirname($r->getFilename()).'/Resources/translations';
}
if (class_exists('Symfony\Component\Form\Form')) {
$r = new \ReflectionClass('Symfony\Component\Form\Form');
$dirs[] = dirname($r->getFilename()).'/../../Resources/translations';
}
$overridePath = $container->getParameter('kernel.root_dir').'/Resources/%s/translations';
foreach ($container->getParameter('kernel.bundles') as $bundle => $class) {
$reflection = new \ReflectionClass($class);
if (is_dir($dir = dirname($reflection->getFilename()).'/Resources/translations')) {
$dirs[] = $dir;
}
if (is_dir($dir = sprintf($overridePath, $bundle))) {
$dirs[] = $dir;
}
}
if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/translations')) {
$dirs[] = dirname($r->getFilename()).'/Resources/translations';
}
if (class_exists('Symfony\Component\Security\Core\Exception\AuthenticationException')) {
$r = new \ReflectionClass('Symfony\Component\Security\Core\Exception\AuthenticationException');
$dirs[] = dirname($r->getFilename()).'/../../Resources/translations';
}
$overridePath = $container->getParameter('kernel.root_dir').'/Resources/%s/translations';
foreach ($container->getParameter('kernel.bundles') as $bundle => $class) {
$reflection = new \ReflectionClass($class);
if (is_dir($dir = dirname($reflection->getFilename()).'/Resources/translations')) {
$dirs[] = $dir;
}
if (is_dir($dir = sprintf($overridePath, $bundle))) {
$dirs[] = $dir;
}
}
if (is_dir($dir = $container->getParameter('kernel.root_dir').'/Resources/translations')) {
$dirs[] = $dir;
}
// Register translation resources
if ($dirs) {
foreach ($dirs as $dir) {
$container->addResource(new DirectoryResource($dir));
}
$finder = Finder::create()
->files()
->filter(function (\SplFileInfo $file) {
return 2 === substr_count($file->getBasename(), '.') && preg_match('/\.\w+$/', $file->getBasename());
})
->in($dirs)
;
// Register translation resources
if ($dirs) {
foreach ($dirs as $dir) {
$container->addResource(new DirectoryResource($dir));
}
$finder = Finder::create()
->files()
->filter(function (\SplFileInfo $file) {
return 2 === substr_count($file->getBasename(), '.') && preg_match('/\.\w+$/', $file->getBasename());
})
->in($dirs)
;
foreach ($finder as $file) {
// filename is domain.locale.format
list($domain, $locale, $format) = explode('.', $file->getBasename(), 3);
$translator->addMethodCall('addResource', array($format, (string) $file, $locale, $domain));
}
foreach ($finder as $file) {
// filename is domain.locale.format
list($domain, $locale, $format) = explode('.', $file->getBasename(), 3);
$translator->addMethodCall('addResource', array($format, (string) $file, $locale, $domain));
}
}
}
@ -598,6 +594,10 @@ class FrameworkExtension extends Extension
*/
private function registerValidationConfiguration(array $config, ContainerBuilder $container, XmlFileLoader $loader)
{
if (!$this->isConfigEnabled($container, $config)) {
return;
}
$loader->load('validator.xml');
$container->setParameter('validator.translation_domain', $config['translation_domain']);

View File

@ -16,46 +16,41 @@ use Symfony\Component\Config\Definition\Processor;
class ConfigurationTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getTestConfigTreeData
*/
public function testConfigTree($options, $results)
public function testDefaultConfig()
{
$processor = new Processor();
$configuration = new Configuration(array());
$config = $processor->processConfiguration($configuration, array($options));
$config = $processor->processConfiguration(new Configuration(), array(array('secret' => 's3cr3t')));
$this->assertEquals($results, $config);
}
public function getTestConfigTreeData()
{
return array(
array(array('secret' => 's3cr3t'), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
$this->assertEquals(
array_merge(array('secret' => 's3cr3t'), self::getBundleDefaultConfig()),
$config
);
}
/**
* @dataProvider getTestValidTrustedProxiesData
*/
public function testValidTrustedProxies($options, $results)
public function testValidTrustedProxies($trustedProxies, $processedProxies)
{
$processor = new Processor();
$configuration = new Configuration(array());
$config = $processor->processConfiguration($configuration, array($options));
$config = $processor->processConfiguration($configuration, array(array(
'secret' => 's3cr3t',
'trusted_proxies' => $trustedProxies
)));
$this->assertEquals($results, $config);
$this->assertEquals($processedProxies, $config['trusted_proxies']);
}
public function getTestValidTrustedProxiesData()
{
return array(
array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('secret' => 's3cr3t', 'trusted_proxies' => array('::1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1', '::1')), array('secret' => 's3cr3t', 'trusted_proxies' => array('127.0.0.1', '::1'), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('secret' => 's3cr3t', 'trusted_proxies' => null), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('secret' => 's3cr3t', 'trusted_proxies' => false), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('secret' => 's3cr3t', 'trusted_proxies' => array()), array('secret' => 's3cr3t', 'trusted_proxies' => array(), 'trust_proxy_headers' => false, 'ide' => NULL, 'annotations' => array('cache' => 'file', 'file_cache_dir' => '%kernel.cache_dir%/annotations', 'debug' => '%kernel.debug%'), 'default_locale' => 'en', 'charset' => null)),
array(array('127.0.0.1'), array('127.0.0.1')),
array(array('::1'), array('::1')),
array(array('127.0.0.1', '::1'), array('127.0.0.1', '::1')),
array(null, array()),
array(false, array()),
array(array(), array()),
);
}
@ -66,7 +61,12 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
{
$processor = new Processor();
$configuration = new Configuration(array());
$config = $processor->processConfiguration($configuration, array(array('secret' => 's3cr3t', 'trusted_proxies' => 'Not an IP address')));
$processor->processConfiguration($configuration, array(
array(
'secret' => 's3cr3t',
'trusted_proxies' => 'Not an IP address'
)
));
}
/**
@ -76,6 +76,55 @@ class ConfigurationTest extends \PHPUnit_Framework_TestCase
{
$processor = new Processor();
$configuration = new Configuration(array());
$config = $processor->processConfiguration($configuration, array(array('secret' => 's3cr3t', 'trusted_proxies' => array('Not an IP address'))));
$processor->processConfiguration($configuration, array(
array(
'secret' => 's3cr3t',
'trusted_proxies' => array('Not an IP address')
)
));
}
protected static function getBundleDefaultConfig()
{
return array(
'charset' => null,
'trust_proxy_headers' => false,
'trusted_proxies' => array(),
'ide' => null,
'default_locale' => 'en',
'form' => array('enabled' => false),
'csrf_protection' => array(
'enabled' => false,
'field_name' => '_token',
),
'esi' => array('enabled' => false),
'router_proxy' => array(
'enabled' => false,
'path' => '/_proxy',
),
'profiler' => array(
'enabled' => false,
'only_exceptions' => false,
'only_master_requests' => false,
'dsn' => 'file:%kernel.cache_dir%/profiler',
'username' => '',
'password' => '',
'lifetime' => 86400,
),
'translator' => array(
'enabled' => false,
'fallback' => 'en',
),
'validation' => array(
'enabled' => false,
'enable_annotations' => false,
'translation_domain' => 'validators',
),
'annotations' => array(
'cache' => 'file',
'file_cache_dir' => '%kernel.cache_dir%/annotations',
'debug' => '%kernel.debug%',
),
);
}
}

View File

@ -3,7 +3,7 @@
$container->loadFromExtension('framework', array(
'secret' => 's3cr3t',
'validation' => array(
'enabled' => true,
'enabled' => true,
'enable_annotations' => true,
),
));

View File

@ -4,6 +4,9 @@ CHANGELOG
2.2.0
-----
* added ArrayNodeDefinition::canBeEnabled() and ArrayNodeDefinition::canBeDisabled()
to ease configuration when some sections are respectively disabled / enabled
by default.
* added a `normalizeKeys()` method for array nodes (to avoid key normalization)
* added numerical type handling for config definitions
* added convenience methods for optional configuration sections to ArrayNodeDefinition

View File

@ -214,16 +214,32 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition
/**
* Adds an "enabled" boolean to enable the current section.
*
* By default, the section is disabled.
* By default, the section is disabled. If any configuration is specified then
* the node will be automatically enabled:
*
* enableableArrayNode: {enabled: true, ...} # The config is enabled & default values get overridden
* enableableArrayNode: ~ # The config is enabled & use the default values
* enableableArrayNode: true # The config is enabled & use the default values
* enableableArrayNode: {other: value, ...} # The config is enabled & default values get overridden
* enableableArrayNode: {enabled: false, ...} # The config is disabled
* enableableArrayNode: false # The config is disabled
*
* @return ArrayNodeDefinition
*/
public function canBeEnabled()
{
$this
->addDefaultsIfNotSet()
->treatFalseLike(array('enabled' => false))
->treatTrueLike(array('enabled' => true))
->treatNullLike(array('enabled' => true))
->beforeNormalization()
->ifArray()
->then(function($v) {
$v['enabled'] = isset($v['enabled']) ? $v['enabled'] : true;
return $v;
})
->end()
->children()
->booleanNode('enabled')
->defaultFalse()
@ -242,6 +258,7 @@ class ArrayNodeDefinition extends NodeDefinition implements ParentNodeDefinition
public function canBeDisabled()
{
$this
->addDefaultsIfNotSet()
->treatFalseLike(array('enabled' => false))
->treatTrueLike(array('enabled' => true))
->treatNullLike(array('enabled' => true))

View File

@ -79,18 +79,19 @@ class Processor
$plural = $key.'s';
}
$values = array();
if (isset($config[$plural])) {
$values = $config[$plural];
} elseif (isset($config[$key])) {
if (is_string($config[$key]) || !is_int(key($config[$key]))) {
// only one
$values = array($config[$key]);
} else {
$values = $config[$key];
}
return $config[$plural];
}
return $values;
if (isset($config[$key])) {
if (is_string($config[$key]) || !is_int(key($config[$key]))) {
// only one
return array($config[$key]);
}
return $config[$key];
}
return array();
}
}

View File

@ -25,19 +25,13 @@ class ArrayNodeTest extends \PHPUnit_Framework_TestCase
}
/**
* normalize() should protect against child values with no corresponding node
* @expectedException Symfony\Component\Config\Definition\Exception\InvalidConfigurationException
* @expectedExceptionMessage Unrecognized options "foo" under "root"
*/
public function testExceptionThrownOnUnrecognizedChild()
{
$node = new ArrayNode('root');
try {
$node->normalize(array('foo' => 'bar'));
$this->fail('An exception should have been throw for a bad child node');
} catch (\Exception $e) {
$this->assertInstanceOf('Symfony\Component\Config\Definition\Exception\InvalidConfigurationException', $e);
$this->assertEquals('Unrecognized options "foo" under "root"', $e->getMessage());
}
$node->normalize(array('foo' => 'bar'));
}
/**

View File

@ -12,6 +12,7 @@
namespace Symfony\Component\Config\Tests\Definition\Builder;
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
use Symfony\Component\Config\Definition\Processor;
use Symfony\Component\Config\Definition\Builder\ScalarNodeDefinition;
use Symfony\Component\Config\Definition\Exception\InvalidDefinitionException;
@ -62,7 +63,10 @@ class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase
public function testConcreteNodeSpecificOption()
{
$node = new ArrayNodeDefinition('root');
$node->addDefaultsIfNotSet()->prototype('array');
$node
->addDefaultsIfNotSet()
->prototype('array')
;
$node->getNode();
}
@ -149,6 +153,50 @@ class ArrayNodeDefinitionTest extends \PHPUnit_Framework_TestCase
$node->getNode();
}
public function testEnabledNodeDefaults()
{
$node = new ArrayNodeDefinition('root');
$node
->canBeEnabled()
->children()
->scalarNode('foo')->defaultValue('bar')->end()
;
$this->assertEquals(array('enabled' => false, 'foo' => 'bar'), $node->getNode()->getDefaultValue());
}
/**
* @dataProvider getEnableableNodeFixtures
*/
public function testTrueEnableEnabledNode($expected, $config, $message)
{
$processor = new Processor();
$node = new ArrayNodeDefinition('root');
$node
->canBeEnabled()
->children()
->scalarNode('foo')->defaultValue('bar')->end()
;
$this->assertEquals(
$expected,
$processor->process($node->getNode(), $config),
$message
);
}
public function getEnableableNodeFixtures()
{
return array(
array(array('enabled' => true, 'foo' => 'bar'), array(true), 'true enables an enableable node'),
array(array('enabled' => true, 'foo' => 'bar'), array(null), 'null enables an enableable node'),
array(array('enabled' => true, 'foo' => 'bar'), array(array('enabled' => true)), 'An enableable node can be enabled'),
array(array('enabled' => true, 'foo' => 'baz'), array(array('foo' => 'baz')), 'any configuration enables an enableable node'),
array(array('enabled' => false, 'foo' => 'baz'), array(array('foo' => 'baz', 'enabled' => false)), 'An enableable node can be disabled'),
array(array('enabled' => false, 'foo' => 'bar'), array(false), 'false disables an enableable node'),
);
}
protected function getField($object, $field)
{
$reflection = new \ReflectionProperty($object, $field);

View File

@ -4,6 +4,7 @@ CHANGELOG
2.2.0
-----
* added Extension::isConfigEnabled() to ease working with enableable configurations
* added an Extension base class with sensible defaults to be used in conjunction
with the Config component.
* added PrependExtensionInterface (to be able to allow extensions to prepend

View File

@ -12,6 +12,8 @@
namespace Symfony\Component\DependencyInjection\Extension;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\DependencyInjection\Exception\BadMethodCallException;
use Symfony\Component\DependencyInjection\Exception\InvalidArgumentException;
use Symfony\Component\Config\Resource\FileResource;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\Config\Definition\Processor;
@ -62,26 +64,19 @@ abstract class Extension implements ExtensionInterface, ConfigurationExtensionIn
*
* @return string The alias
*
* @throws \BadMethodCallException When the extension name does not follow conventions
* @throws BadMethodCallException When the extension name does not follow conventions
*/
public function getAlias()
{
$className = get_class($this);
if (substr($className, -9) != 'Extension') {
throw new \BadMethodCallException('This extension does not follow the naming convention; you must overwrite the getAlias() method.');
throw new BadMethodCallException('This extension does not follow the naming convention; you must overwrite the getAlias() method.');
}
$classBaseName = substr(strrchr($className, '\\'), 1, -9);
return Container::underscore($classBaseName);
}
final protected function processConfiguration(ConfigurationInterface $configuration, array $configs)
{
$processor = new Processor();
return $processor->processConfiguration($configuration, $configs);
}
/**
* {@inheritDoc}
*/
@ -104,4 +99,28 @@ abstract class Extension implements ExtensionInterface, ConfigurationExtensionIn
return null;
}
final protected function processConfiguration(ConfigurationInterface $configuration, array $configs)
{
$processor = new Processor();
return $processor->processConfiguration($configuration, $configs);
}
/**
* @param ContainerBuilder $container
* @param array $config
*
* @return Boolean Whether the configuration is enabled
*
* @throws InvalidArgumentException When the config is not enableable
*/
protected function isConfigEnabled(ContainerBuilder $container, array $config)
{
if (!array_key_exists('enabled', $config)) {
throw new InvalidArgumentException("The config array has no 'enabled' key.");
}
return (Boolean) $container->getParameterBag()->resolveValue($config['enabled']);
}
}

View File

@ -0,0 +1,81 @@
<?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\DependencyInjection\Tests\Extension;
class ExtensionTest extends \PHPUnit_Framework_TestCase
{
/**
* @dataProvider getResolvedEnabledFixtures
*/
public function testIsConfigEnabledReturnsTheResolvedValue($enabled)
{
$pb = $this->getMockBuilder('Symfony\Component\DependencyInjection\ParameterBag\ParameterBag')
->setMethods(array('resolveValue'))
->getMock()
;
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')
->setMethods(array('getParameterBag'))
->getMock()
;
$pb->expects($this->once())
->method('resolveValue')
->with($this->equalTo($enabled))
->will($this->returnValue($enabled))
;
$container->expects($this->once())
->method('getParameterBag')
->will($this->returnValue($pb))
;
$extension = $this->getMockBuilder('Symfony\Component\DependencyInjection\Extension\Extension')
->setMethods(array())
->getMockForAbstractClass()
;
$r = new \ReflectionMethod('Symfony\Component\DependencyInjection\Extension\Extension', 'isConfigEnabled');
$r->setAccessible(true);
$r->invoke($extension, $container, array('enabled' => $enabled));
}
public function getResolvedEnabledFixtures()
{
return array(
array(true),
array(false)
);
}
/**
* @expectedException \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException
* @expectedExceptionMessage The config array has no 'enabled' key.
*/
public function testIsConfigEnabledOnNonEnableableConfig()
{
$container = $this->getMockBuilder('Symfony\Component\DependencyInjection\ContainerBuilder')
->getMock()
;
$extension = $this->getMockBuilder('Symfony\Component\DependencyInjection\Extension\Extension')
->setMethods(array())
->getMockForAbstractClass()
;
$r = new \ReflectionMethod('Symfony\Component\DependencyInjection\Extension\Extension', 'isConfigEnabled');
$r->setAccessible(true);
$r->invoke($extension, $container, array());
}
}