[Security/DependencyInjection] adds support for merging security configurations

The merging is done in three steps:

    1. Normalization:
    =================
    All passed config arrays will be transformed into the same structure
    regardless of what format they come from.

    2. Merging:
    ===========
    This is the step when the actual merging is performed. Starting at the root
    the configs will be passed along the tree until a node has no children, or
    the merging of sub-paths of the current node has been specifically disabled.

       Left-Side       Right-Side      Merge Result
       ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
       -nothing-       array           Right-Side will be taken.
       scalar          scalar          Right-Side will be taken.
       array           false           Right-Side will be taken if ->canBeUnset()
                                       was called on the array node.
       false           array           Right-Side will be taken.
       array           array           Each value in the array will be passed to
                                       the specific child node, or the prototype
                                       node (whatever is present).

    3. Finalization:
    ================
    The normalized, and merged config will be passed through the config tree to
    perform final validation on the submitted values, and set default values
    where this has been requested.

You can influence this process in various ways, here is a list with some examples.
All of these methods must be called on the node on which they should be applied.

  * isRequired(): Node must be present in at least one config file.
  * requiresAtLeastOneElement(): PrototypeNode must have at least one element.
  * treatNullLike($value): Replaces null with $value during normalization.
  * treatTrueLike($value): Same as above just for true
  * treatFalseLike($value): Same as above just for false
  * defaultValue($value): Sets a default value for this node (only for scalars)
  * addDefaultsIfNotSet(): Whether to add default values of an array which has not
                           been defined in any configuration file.
  * disallowNewKeysInSubsequentConfigs(): All keys for this array must be defined
                                          in one configuration file, subsequent
                                          configurations may only overwrite these.
  * fixXmlConfig($key, $plural = null): Transforms XML config into same structure
                                        as YAML, and PHP configurations.
  * useAttributeAsKey($name): Defines which XML attribute to use as array key.
  * cannotBeOverwritten(): Declares a certain sub-path as non-overwritable. All
                           configuration for this path must be defined in the same
                           configuration file.
  * cannotBeEmpty(): If value is set, it must be non-empty.
  * canBeUnset(): If array values should be unset if false is specified.

Architecture:
=============
The configuration consists basically out of two different sets of classes.

  1. Builder classes: These classes provide the fluent interface and
                      are used to construct the config tree.

  2. Node classes: These classes contain the actual logic for normalization,
                   merging, and finalizing configurations.

After you have added all the metadata to your builders, the call to
->buildTree() will convert this metadata to actual node classes. Most of the
time, you will not have to interact with the config nodes directly, but will
delegate this to the Processor class which will call the respective methods
on the config node classes.
This commit is contained in:
Johannes Schmitt 2011-02-04 14:37:01 +01:00 committed by Fabien Potencier
parent c5fb96b86b
commit 0b8fef2347
41 changed files with 1721 additions and 437 deletions

View File

@ -0,0 +1,234 @@
<?php
namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
/**
* This class contains the configuration information for the following tags:
*
* * security.config
* * security.acl
*
* This information is solely responsible for how the different configuration
* sections are normalized, and merged.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Configuration
{
public function getAclConfigTree()
{
$tb = new TreeBuilder();
return $tb
->root('security:acl', 'array')
->scalarNode('connection')->end()
->scalarNode('cache')->end()
->end()
->buildTree();
}
public function getFactoryConfigTree()
{
$tb = new TreeBuilder();
return $tb
->root('security:config', 'array')
->fixXmlConfig('factory', 'factories')
->arrayNode('factories')
->prototype('scalar')->end()
->end()
->end()
->buildTree();
}
public function getMainConfigTree(array $factories)
{
$tb = new TreeBuilder();
$rootNode = $tb->root('security:config', 'array');
$rootNode
->scalarNode('access_denied_url')->end()
->scalarNode('session_fixation_strategy')->cannotBeEmpty()->defaultValue('migrate')->end()
;
$this->addEncodersSection($rootNode);
$this->addProvidersSection($rootNode);
$this->addFirewallsSection($rootNode, $factories);
$this->addAccessControlSection($rootNode);
$this->addRoleHierarchySection($rootNode);
return $tb->buildTree();
}
protected function addRoleHierarchySection($rootNode)
{
$rootNode
->fixXmlConfig('role', 'role_hierarchy')
->arrayNode('role_hierarchy')
->containsNameValuePairsWithKeyAttribute('id')
->prototype('array')
->beforeNormalization()->ifString()->then(function($v) { return array('value' => $v); })->end()
->beforeNormalization()
->ifTrue(function($v) { return is_array($v) && isset($v['value']); })
->then(function($v) { return preg_split('/\s*,\s*/', $v['value']); })
->end()
->prototype('scalar')->end()
->end()
->end()
;
}
protected function addAccessControlSection($rootNode)
{
$rootNode
->fixXmlConfig('rule', 'access_control')
->arrayNode('access_control')
->cannotBeOverwritten()
->prototype('array')
->scalarNode('requires_channel')->defaultNull()->end()
->scalarNode('path')->defaultNull()->end()
->scalarNode('host')->defaultNull()->end()
->scalarNode('ip')->defaultNull()->end()
->arrayNode('methods')
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->fixXmlConfig('role')
->arrayNode('roles')
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->fixXmlConfig('attribute')
->arrayNode('attributes')
->containsNameValuePairsWithKeyAttribute('key')
->prototype('scalar')
->beforeNormalization()
->ifTrue(function($v) { return is_array($v) && isset($v['pattern']); })
->then(function($v) { return $v['pattern']; })
->end()
->end()
->end()
->end()
->end()
;
}
protected function addFirewallsSection($rootNode, array $factories)
{
$firewallNodeBuilder =
$rootNode
->fixXmlConfig('firewall')
->arrayNode('firewalls')
->disallowNewKeysInSubsequentConfigs()
->useAttributeAsKey('name')
->prototype('array')
->scalarNode('pattern')->end()
->booleanNode('security')->defaultTrue()->end()
->scalarNode('request_matcher')->end()
->scalarNode('access_denied_url')->end()
->scalarNode('access_denied_handler')->end()
->scalarNode('entry_point')->end()
->scalarNode('provider')->end()
->booleanNode('stateless')->defaultFalse()->end()
->scalarNode('context')->cannotBeEmpty()->end()
->arrayNode('logout')
->treatTrueLike(array())
->canBeUnset()
->scalarNode('path')->defaultValue('/logout')->end()
->scalarNode('target')->defaultValue('/')->end()
->booleanNode('invalidate_session')->defaultTrue()->end()
->fixXmlConfig('delete_cookie')
->arrayNode('delete_cookies')
->beforeNormalization()
->ifTrue(function($v) { return is_array($v) && is_int(key($v)); })
->then(function($v) { return array_map(function($v) { return array('name' => $v); }, $v); })
->end()
->useAttributeAsKey('name')
->prototype('array')
->scalarNode('path')->defaultNull()->end()
->scalarNode('domain')->defaultNull()->end()
->end()
->end()
->fixXmlConfig('handler')
->arrayNode('handlers')
->prototype('scalar')->end()
->end()
->end()
->booleanNode('anonymous')->end()
->arrayNode('switch_user')
->scalarNode('provider')->end()
->scalarNode('parameter')->defaultValue('_switch_user')->end()
->scalarNode('role')->defaultValue('ROLE_ALLOWED_TO_SWITCH')->end()
->end()
;
foreach ($factories as $factoriesAtPosition) {
foreach ($factoriesAtPosition as $factory) {
$factoryNode =
$firewallNodeBuilder->arrayNode(str_replace('-', '_', $factory->getKey()))
->canBeUnset()
;
$factory->addConfiguration($factoryNode);
}
}
}
protected function addProvidersSection($rootNode)
{
$rootNode
->fixXmlConfig('provider')
->arrayNode('providers')
->disallowNewKeysInSubsequentConfigs()
->requiresAtLeastOneElement()
->useAttributeAsKey('name')
->prototype('array')
->scalarNode('id')->end()
->fixXmlConfig('provider')
->arrayNode('providers')
->prototype('scalar')->end()
->end()
->fixXmlConfig('user')
->arrayNode('users')
->useAttributeAsKey('name')
->prototype('array')
->scalarNode('password')->defaultValue(uniqid())->end()
->arrayNode('roles')
->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end()
->prototype('scalar')->end()
->end()
->end()
->end()
->arrayNode('entity')
->scalarNode('class')->isRequired()->cannotBeEmpty()->end()
->scalarNode('property')->defaultNull()->end()
->end()
->arrayNode('document')
->scalarNode('class')->isRequired()->cannotBeEmpty()->end()
->scalarNode('property')->defaultNull()->end()
->end()
->end()
->end()
;
}
protected function addEncodersSection($rootNode)
{
$rootNode
->fixXmlConfig('encoder')
->arrayNode('encoders')
->useAttributeAsKey('class')
->prototype('array')
->beforeNormalization()->ifString()->then(function($v) { return array('algorithm' => $v); })->end()
->scalarNode('algorithm')->isRequired()->cannotBeEmpty()->end()
->booleanNode('ignore_case')->end()
->booleanNode('encode_as_base64')->end()
->scalarNode('iterations')->end()
->scalarNode('id')->end()
->end()
->end()
;
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -38,10 +40,6 @@ abstract class AbstractFactory implements SecurityFactoryInterface
public function create(ContainerBuilder $container, $id, $config, $userProviderId, $defaultEntryPointId)
{
if (!is_array($config)) {
$config = array();
}
// authentication provider
$authProviderId = $this->createAuthProvider($container, $id, $config, $userProviderId);
$container
@ -66,6 +64,24 @@ abstract class AbstractFactory implements SecurityFactoryInterface
return array($authProviderId, $listenerId, $entryPointId);
}
public function addConfiguration(NodeBuilder $node)
{
$node
->scalarNode('provider')->end()
->booleanNode('remember_me')->defaultTrue()->end()
->scalarNode('success_handler')->end()
->scalarNode('failure_handler')->end()
;
foreach ($this->options as $name => $default) {
if (is_bool($default)) {
$node->booleanNode($name)->defaultValue($default);
} else {
$node->scalarNode($name)->defaultValue($default);
}
}
}
public final function addOption($name, $default = null)
{
$this->options[$name] = $default;
@ -127,18 +143,15 @@ abstract class AbstractFactory implements SecurityFactoryInterface
*/
protected function isRememberMeAware($config)
{
return !isset($config['remember_me']) || (Boolean) $config['remember_me'];
return $config['remember_me'];
}
protected function createListener($container, $id, $config, $userProvider)
{
// merge set options with default options
$options = $this->getOptionsFromConfig($config);
$listenerId = $this->getListenerId();
$listener = new DefinitionDecorator($listenerId);
$listener->setArgument(3, $id);
$listener->setArgument(4, $options);
$listener->setArgument(4, array_intersect_key($config, $this->options));
// success handler
if (isset($config['success_handler'])) {
@ -155,17 +168,4 @@ abstract class AbstractFactory implements SecurityFactoryInterface
return $listenerId;
}
protected final function getOptionsFromConfig($config)
{
$options = $this->options;
foreach (array_keys($options) as $key) {
if (array_key_exists($key, $config)) {
$options[$key] = $config[$key];
}
}
return $options;
}
}

View File

@ -59,14 +59,11 @@ class FormLoginFactory extends AbstractFactory
protected function createEntryPoint($container, $id, $config, $defaultEntryPoint)
{
// merge set options with default options
$options = $this->getOptionsFromConfig($config);
$entryPointId = 'security.authentication.form_entry_point.'.$id;
$container
->setDefinition($entryPointId, new DefinitionDecorator('security.authentication.form_entry_point'))
->addArgument($options['login_path'])
->addArgument($options['use_forward'])
->addArgument($config['login_path'])
->addArgument($config['use_forward'])
;
return $entryPointId;

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -53,4 +55,11 @@ class HttpBasicFactory implements SecurityFactoryInterface
{
return 'http-basic';
}
public function addConfiguration(NodeBuilder $builder)
{
$builder
->scalarNode('provider')->end()
;
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
@ -53,4 +55,11 @@ class HttpDigestFactory implements SecurityFactoryInterface
{
return 'http-digest';
}
public function addConfiguration(NodeBuilder $builder)
{
$builder
->scalarNode('provider')->end()
;
}
}

View File

@ -2,6 +2,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Reference;
@ -10,16 +12,19 @@ use Symfony\Component\DependencyInjection\ContainerBuilder;
class RememberMeFactory implements SecurityFactoryInterface
{
protected $options = array(
'name' => 'REMEMBERME',
'lifetime' => 31536000,
'path' => '/',
'domain' => null,
'secure' => false,
'httponly' => true,
'always_remember_me' => false,
'remember_me_parameter' => '_remember_me',
);
public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
{
if (!isset($config['key']) || empty($config['key'])) {
throw new \RuntimeException('A "key" must be defined for each remember-me section.');
}
if (isset($config['provider'])) {
throw new \RuntimeException('You must not set a user provider for remember-me.');
}
// authentication provider
$authProviderId = 'security.authentication.provider.rememberme.'.$id;
$container
@ -60,22 +65,7 @@ class RememberMeFactory implements SecurityFactoryInterface
}
// remember-me options
$options = array(
'name' => 'REMEMBERME',
'lifetime' => 31536000,
'path' => '/',
'domain' => null,
'secure' => false,
'httponly' => true,
'always_remember_me' => false,
'remember_me_parameter' => '_remember_me',
);
foreach ($options as $name => $option) {
if (array_key_exists($name, $config)) {
$options[$name] = $config[$name];
}
}
$rememberMeServices->setArgument(3, $options);
$rememberMeServices->setArgument(3, array_intersect_key($config, $this->options));
// attach to remember-me aware listeners
$userProviders = array();
@ -118,4 +108,20 @@ class RememberMeFactory implements SecurityFactoryInterface
{
return 'remember-me';
}
public function addConfiguration(NodeBuilder $node)
{
$node
->scalarNode('key')->isRequired()->cannotBeEmpty()->end()
->scalarNode('token_provider')->end()
;
foreach ($this->options as $name => $value) {
if (is_bool($value)) {
$node->booleanNode($name)->defaultValue($value);
} else {
$node->scalarNode($name)->defaultValue($value);
}
}
}
}

View File

@ -11,6 +11,7 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\ContainerBuilder;
/**
@ -25,4 +26,6 @@ interface SecurityFactoryInterface
function getPosition();
function getKey();
function addConfiguration(NodeBuilder $builder);
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
use Symfony\Component\DependencyInjection\Configuration\Builder\NodeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
@ -50,4 +52,11 @@ class X509Factory implements SecurityFactoryInterface
{
return 'x509';
}
public function addConfiguration(NodeBuilder $builder)
{
$builder
->scalarNode('provider')->end()
;
}
}

View File

@ -11,6 +11,8 @@
namespace Symfony\Bundle\SecurityBundle\DependencyInjection;
use Symfony\Component\DependencyInjection\Configuration\Processor;
use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
use Symfony\Component\DependencyInjection\DefinitionDecorator;
use Symfony\Component\DependencyInjection\Alias;
use Symfony\Component\HttpKernel\DependencyInjection\Extension;
@ -34,19 +36,82 @@ class SecurityExtension extends Extension
protected $requestMatchers = array();
protected $contextListeners = array();
protected $listenerPositions = array('pre_auth', 'form', 'http', 'remember_me');
protected $configuration;
protected $factories;
public function __construct()
{
$this->configuration = new Configuration();
}
public function configLoad(array $configs, ContainerBuilder $container)
{
foreach ($configs as $config) {
$this->doConfigLoad($this->normalizeKeys($config), $container);
$processor = new Processor();
// first assemble the factories
$factories = $this->createListenerFactories($container, $processor->process($this->configuration->getFactoryConfigTree(), $configs));
// normalize and merge the actual configuration
$tree = $this->configuration->getMainConfigTree($factories);
$config = $processor->process($tree, $configs);
// load services
$loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security.xml');
$loader->load('security_listeners.xml');
$loader->load('security_rememberme.xml');
$loader->load('templating_php.xml');
$loader->load('templating_twig.xml');
$loader->load('collectors.xml');
// set some global scalars
if (isset($config['access_denied_url'])) {
$container->setParameter('security.access.denied_url', $config['access_denied_url']);
}
if (isset($config['session_fixation_protection'])) {
$container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_protection']);
}
$this->createFirewalls($config, $container);
$this->createAuthorization($config, $container);
$this->createRoleHierarchy($config, $container);
}
public function aclLoad(array $configs, ContainerBuilder $container)
{
foreach ($configs as $config) {
$this->doAclLoad($this->normalizeKeys($config), $container);
$processor = new Processor();
$config = $processor->process($this->configuration->getAclConfigTree(), $configs);
$loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security_acl.xml');
if (isset($config['connection'])) {
$container->setAlias('security.acl.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection']));
}
if (isset($config['cache'])) {
$container->setAlias('security.acl.cache', sprintf('security.acl.cache.%s', $config['cache']));
}
}
/**
* Returns the base path for the XSD files.
*
* @return string The XSD base path
*/
public function getXsdValidationBasePath()
{
return __DIR__.'/../Resources/config/schema';
}
public function getNamespace()
{
return 'http://www.symfony-project.org/schema/dic/security';
}
public function getAlias()
{
return 'security';
}
/**
@ -55,121 +120,50 @@ class SecurityExtension extends Extension
* @param array $config An array of configuration settings
* @param ContainerBuilder $container A ContainerBuilder instance
*/
protected function doConfigLoad($config, ContainerBuilder $container)
{
if (!$container->hasDefinition('security.context')) {
$loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security.xml');
$loader->load('security_listeners.xml');
$loader->load('security_rememberme.xml');
$loader->load('templating_php.xml');
$loader->load('templating_twig.xml');
$loader->load('collectors.xml');
}
if (isset($config['access_denied_url'])) {
$container->setParameter('security.access.denied_url', $config['access_denied_url']);
}
// session fixation protection
if (isset($config['session_fixation_protection'])) {
$container->setParameter('security.authentication.session_strategy.strategy', $config['session_fixation_protection']);
}
$this->createFirewalls($config, $container);
$this->createAuthorization($config, $container);
$this->createRoleHierarchy($config, $container);
return $container;
}
protected function createRoleHierarchy($config, ContainerBuilder $container)
{
$roles = array();
if (isset($config['role_hierarchy'])) {
$roles = $config['role_hierarchy'];
if (!isset($config['role_hierarchy'])) {
return;
}
if (isset($roles['role']) && is_int(key($roles['role']))) {
$roles = $roles['role'];
}
$hierarchy = array();
foreach ($roles as $id => $role) {
if (is_array($role) && isset($role['id'])) {
$id = $role['id'];
}
$value = $role;
if (is_array($role) && isset($role['value'])) {
$value = $role['value'];
}
$hierarchy[$id] = is_array($value) ? $value : preg_split('/\s*,\s*/', $value);
}
$container->setParameter('security.role_hierarchy.roles', $hierarchy);
$container->setParameter('security.role_hierarchy.roles', $config['role_hierarchy']);
$container->remove('security.access.simple_role_voter');
$container->getDefinition('security.access.role_hierarchy_voter')->addTag('security.voter');
}
protected function createAuthorization($config, ContainerBuilder $container)
{
$rules = array();
if (isset($config['access_control'])) {
$rules = $config['access_control'];
if (!isset($config['access_control'])) {
return;
}
if (isset($rules['rule']) && is_array($rules['rule'])) {
$rules = $rules['rule'];
}
foreach ($config['access_control'] as $access) {
$matcher = $this->createRequestMatcher(
$container,
$access['path'],
$access['host'],
count($access['methods']) === 0 ? null : $access['methods'],
$access['ip'],
$access['attributes']
);
foreach ($rules as $i => $access) {
$roles = isset($access['role']) ? (is_array($access['role']) ? $access['role'] : preg_split('/\s*,\s*/', $access['role'])) : array();
$channel = null;
if (isset($access['requires_channel'])) {
$channel = $access['requires_channel'];
}
// matcher
$path = $host = $methods = $ip = null;
if (isset($access['path'])) {
$path = $access['path'];
}
if (isset($access['host'])) {
$host = $access['host'];
}
if (count($tMethods = $this->normalizeConfig($access, 'method')) > 0) {
$methods = $tMethods;
}
if (isset($access['ip'])) {
$ip = $access['ip'];
}
$matchAttributes = array();
$attributes = $this->normalizeConfig($access, 'attribute');
foreach ($attributes as $key => $attribute) {
if (isset($attribute['key'])) {
$key = $attribute['key'];
}
$matchAttributes[$key] = $attribute['pattern'];
}
$matcher = $this->createRequestMatcher($container, $path, $host, $methods, $ip, $matchAttributes);
$container->getDefinition('security.access_map')->addMethodCall('add', array($matcher, $roles, $channel));
$container->getDefinition('security.access_map')
->addMethodCall('add', array($matcher, $access['roles'], $access['requires_channel']));
}
}
protected function createFirewalls($config, ContainerBuilder $container)
{
if (!isset($config['firewalls'])) {
return;
}
$firewalls = $config['firewalls'];
$providerIds = $this->createUserProviders($config, $container);
$this->createEncoders($config, $container);
if (!$firewalls = $this->normalizeConfig($config, 'firewall')) {
return;
}
// make the ContextListener aware of the configured user providers
$definition = $container->getDefinition('security.context_listener');
$arguments = $definition->getArguments();
@ -185,16 +179,8 @@ class SecurityExtension extends Extension
// load firewall map
$mapDef = $container->getDefinition('security.firewall.map');
$names = $map = array();
$map = array();
foreach ($firewalls as $name => $firewall) {
if (isset($firewall['name'])) {
$name = $firewall['name'];
}
if (in_array($name, $names)) {
throw new \RuntimeException(sprintf('The firewall name must be unique. Duplicate found: "%s"', $name));
}
$names[] = $name;
list($matcher, $listeners, $exceptionListener) = $this->createFirewall($container, $name, $firewall, $providerIds, $factories);
$contextId = 'security.firewall.map.context.'.$name;
@ -220,7 +206,7 @@ class SecurityExtension extends Extension
}
// Security disabled?
if (isset($firewall['security']) && !$firewall['security']) {
if (false === $firewall['security']) {
return array($matcher, array(), null);
}
@ -228,9 +214,6 @@ class SecurityExtension extends Extension
if (isset($firewall['provider'])) {
$defaultProvider = $this->getUserProviderId($firewall['provider']);
} else {
if (!$providerIds) {
throw new \InvalidArgumentException('You must provide at least one authentication provider.');
}
$defaultProvider = reset($providerIds);
}
@ -242,7 +225,7 @@ class SecurityExtension extends Extension
$listeners[] = new Reference('security.channel_listener');
// Context serializer listener
if (!isset($firewall['stateless']) || !$firewall['stateless']) {
if (false === $firewall['stateless']) {
$contextKey = $id;
if (isset($firewall['context'])) {
$contextKey = $firewall['context'];
@ -252,44 +235,29 @@ class SecurityExtension extends Extension
}
// Logout listener
if (array_key_exists('logout', $firewall)) {
if (isset($firewall['logout'])) {
$listenerId = 'security.logout_listener.'.$id;
$listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.logout_listener'));
$listener->addArgument($firewall['logout']['path']);
$listener->addArgument($firewall['logout']['target']);
$listeners[] = new Reference($listenerId);
if (!is_array($firewall['logout'])) {
$firewall['logout'] = array();
}
if (isset($firewall['logout']['path'])) {
$listener->setArgument(1, $firewall['logout']['path']);
}
if (isset($firewall['logout']['target'])) {
$listener->setArgument(2, $firewall['logout']['target']);
}
// add session logout handler
$invalidateSession = true;
if (isset($firewall['logout']['invalidate_session'])) {
$invalidateSession = (Boolean) $firewall['logout']['invalidate_session'];
}
if (true === $invalidateSession && (!isset($firewall['stateless']) || !$firewall['stateless'])) {
if (true === $firewall['logout']['invalidate_session'] && false === $firewall['stateless']) {
$listener->addMethodCall('addHandler', array(new Reference('security.logout.handler.session')));
}
// add cookie logout handler
if (count($cookies = $this->normalizeConfig($firewall['logout'], 'cookie')) > 0) {
if (count($firewall['logout']['delete_cookies']) > 0) {
$cookieHandlerId = 'security.logout.handler.cookie_clearing.'.$id;
$cookieHandler = $container->setDefinition($cookieHandlerId, new DefinitionDecorator('security.logout.handler.cookie_clearing'));
$cookieHandler->addArgument($cookies);
$cookieHandler->addArgument($firewall['logout']['delete_cookies']);
$listener->addMethodCall('addHandler', array(new Reference($cookieHandlerId)));
}
// add custom handlers
foreach ($this->normalizeConfig($firewall['logout'], 'handler') as $handlerId) {
foreach ($firewall['logout']['handlers'] as $handlerId) {
$listener->addMethodCall('addHandler', array(new Reference($handlerId)));
}
}
@ -303,7 +271,7 @@ class SecurityExtension extends Extension
$listeners[] = new Reference('security.access_listener');
// Switch user listener
if (array_key_exists('switch_user', $firewall)) {
if (isset($firewall['switch_user'])) {
$listeners[] = new Reference($this->createSwitchUserListener($container, $id, $firewall['switch_user'], $defaultProvider));
}
@ -340,13 +308,9 @@ class SecurityExtension extends Extension
foreach ($this->listenerPositions as $position) {
foreach ($factories[$position] as $factory) {
$key = $factory->getKey();
$keybis = str_replace('-', '_', $key);
$key = str_replace('-', '_', $factory->getKey());
if (array_key_exists($keybis, $firewall)) {
$firewall[$key] = $firewall[$keybis];
}
if (array_key_exists($key, $firewall) && $firewall[$key] !== false) {
if (isset($firewall[$key])) {
$userProvider = isset($firewall[$key]['provider']) ? $this->getUserProviderId($firewall[$key]['provider']) : $defaultProvider;
list($provider, $listenerId, $defaultEntryPoint) = $factory->create($container, $id, $firewall[$key], $userProvider, $defaultEntryPoint);
@ -359,7 +323,7 @@ class SecurityExtension extends Extension
}
// Anonymous
if (array_key_exists('anonymous', $firewall)) {
if (isset($firewall['anonymous'])) {
$listeners[] = new Reference('security.authentication.listener.anonymous');
$hasListeners = true;
}
@ -371,66 +335,14 @@ class SecurityExtension extends Extension
return array($listeners, $providers, $defaultEntryPoint);
}
// Parses user providers and returns an array of their ids
protected function createUserProviders($config, ContainerBuilder $container)
{
$providers = $this->normalizeConfig($config, 'provider');
if (!$providers) {
return array();
}
$providerIds = array();
foreach ($providers as $name => $provider) {
$id = $this->createUserDaoProvider($name, $provider, $container);
if (in_array($id, $providerIds, true)) {
throw new \RuntimeException(sprintf('Provider names must be unique. Duplicate entry for %s.', $id));
}
$providerIds[] = $id;
}
return $providerIds;
}
protected function createListenerFactories(ContainerBuilder $container, $config)
{
// load service templates
$c = new ContainerBuilder();
$parameterBag = $container->getParameterBag();
$loader = new XmlFileLoader($c, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security_factories.xml');
// load user-created listener factories
foreach ($this->normalizeConfig($config, 'factory', 'factories') as $factory) {
$loader->load($parameterBag->resolveValue($factory));
}
$tags = $c->findTaggedServiceIds('security.listener.factory');
$factories = array();
foreach ($this->listenerPositions as $position) {
$factories[$position] = array();
}
foreach (array_keys($tags) as $tag) {
$factory = $c->get($tag);
$factories[$factory->getPosition()][] = $factory;
}
return $factories;
}
protected function createEncoders($config, ContainerBuilder $container)
{
$encoders = $this->normalizeConfig($config, 'encoder');
if (!$encoders) {
return array();
if (!isset($config['encoders'])) {
return;
}
$encoderMap = array();
foreach ($encoders as $class => $encoder) {
foreach ($config['encoders'] as $class => $encoder) {
$encoderMap = $this->createEncoder($encoderMap, $class, $encoder, $container);
}
@ -442,21 +354,6 @@ class SecurityExtension extends Extension
protected function createEncoder(array $encoderMap, $accountClass, $config, ContainerBuilder $container)
{
if (is_array($config) && isset($config['class'])) {
$accountClass = $config['class'];
}
if (empty($accountClass)) {
throw new \RuntimeException('Each encoder needs an account class.');
}
// a minimal message digest, or plaintext encoder
if (is_string($config)) {
$config = array(
'algorithm' => $config,
);
}
// a custom encoder service
if (isset($config['id'])) {
$container
@ -467,17 +364,12 @@ class SecurityExtension extends Extension
return $encoderMap;
}
// a lazy loaded, message digest or plaintext encoder
if (!isset($config['algorithm'])) {
throw new \RuntimeException('"algorithm" must be defined.');
}
// plaintext encoder
if ('plaintext' === $config['algorithm']) {
$arguments = array();
if (isset($config['ignore_case'])) {
$arguments[0] = (Boolean) $config['ignore_case'];
$arguments[0] = $config['ignore_case'];
}
$encoderMap[$accountClass] = array(
@ -493,7 +385,7 @@ class SecurityExtension extends Extension
// add optional arguments
if (isset($config['encode_as_base64'])) {
$arguments[1] = (Boolean) $config['encode_as_base64'];
$arguments[1] = $config['encode_as_base64'];
} else {
$arguments[1] = false;
}
@ -512,17 +404,23 @@ class SecurityExtension extends Extension
return $encoderMap;
}
// Parses user providers and returns an array of their ids
protected function createUserProviders($config, ContainerBuilder $container)
{
$providerIds = array();
foreach ($config['providers'] as $name => $provider) {
$id = $this->createUserDaoProvider($name, $provider, $container);
$providerIds[] = $id;
}
return $providerIds;
}
// Parses a <provider> tag and returns the id for the related user provider service
// FIXME: Replace register() calls in this method with DefinitionDecorator
// and move the actual definition to an xml file
protected function createUserDaoProvider($name, $provider, ContainerBuilder $container, $master = true)
{
if (isset($provider['name'])) {
$name = $provider['name'];
}
if (!$name) {
throw new \RuntimeException('You must define a name for each user provider.');
}
$name = $this->getUserProviderId(strtolower($name));
// Existing DAO service provider
@ -533,7 +431,7 @@ class SecurityExtension extends Extension
}
// Chain provider
if (isset($provider['provider'])) {
if (count($provider['providers']) > 0) {
// FIXME
throw new \RuntimeException('Not implemented yet.');
}
@ -546,8 +444,9 @@ class SecurityExtension extends Extension
->setArguments(array(
new Reference('security.user.entity_manager'),
$provider['entity']['class'],
isset($provider['entity']['property']) ? $provider['entity']['property'] : null,
));
$provider['entity']['property'],
))
;
return $name;
}
@ -560,7 +459,7 @@ class SecurityExtension extends Extension
->setArguments(array(
new Reference('security.user.document_manager'),
$provider['document']['class'],
isset($provider['document']['property']) ? $provider['document']['property'] : null,
$provider['document']['property'],
));
return $name;
@ -569,27 +468,8 @@ class SecurityExtension extends Extension
// In-memory DAO provider
$definition = $container->register($name, '%security.user.provider.in_memory.class%');
$definition->setPublic(false);
foreach ($this->normalizeConfig($provider, 'user') as $username => $user) {
if (isset($user['name'])) {
$username = $user['name'];
}
if (!array_key_exists('password', $user)) {
// if no password is provided explicitly, it means that
// the user will be used with OpenID, X.509 certificates, ...
// Let's generate a random password just to be sure this
// won't be used accidentally with other authentication schemes.
// If you want an empty password, just say so explicitly
$user['password'] = uniqid();
}
if (!isset($user['roles'])) {
$user['roles'] = array();
} else {
$user['roles'] = is_array($user['roles']) ? $user['roles'] : preg_split('/\s*,\s*/', $user['roles']);
}
$userId = $name.'_'.md5(serialize(array($username, $user['password'], $user['roles'])));
foreach ($provider['users'] as $username => $user) {
$userId = $name.'_'.md5(json_encode(array($username, $user['password'], $user['roles'])));
$container
->register($userId, 'Symfony\Component\Security\Core\User\User')
@ -632,14 +512,8 @@ class SecurityExtension extends Extension
$listener = $container->setDefinition($switchUserListenerId, new DefinitionDecorator('security.authentication.switchuser_listener'));
$listener->setArgument(1, new Reference($userProvider));
$listener->setArgument(3, $id);
if (isset($config['parameter'])) {
$listener->setArgument(5, $config['parameter']);
}
if (isset($config['role'])) {
$listener->setArgument(6, $config['role']);
}
$listener->addArgument($config['parameter']);
$listener->addArgument($config['role']);
return $switchUserListenerId;
}
@ -668,42 +542,35 @@ class SecurityExtension extends Extension
return $this->requestMatchers[$id] = new Reference($id);
}
protected function doAclLoad(array $config, ContainerBuilder $container)
protected function createListenerFactories(ContainerBuilder $container, $config)
{
if (!$container->hasDefinition('security.acl')) {
$loader = new XmlFileLoader($container, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security_acl.xml');
if (null !== $this->factories) {
return $this->factories;
}
if (isset($config['connection'])) {
$container->setAlias('security.acl.dbal.connection', sprintf('doctrine.dbal.%s_connection', $config['connection']));
// load service templates
$c = new ContainerBuilder();
$parameterBag = $container->getParameterBag();
$loader = new XmlFileLoader($c, array(__DIR__.'/../Resources/config', __DIR__.'/Resources/config'));
$loader->load('security_factories.xml');
// load user-created listener factories
foreach ($config['factories'] as $factory) {
$loader->load($parameterBag->resolveValue($factory));
}
if (isset($config['cache'])) {
$container->setAlias('security.acl.cache', sprintf('security.acl.cache.%s', $config['cache']));
} else {
$container->remove('security.acl.cache.doctrine');
$container->removeAlias('security.acl.cache.doctrine.cache_impl');
$tags = $c->findTaggedServiceIds('security.listener.factory');
$factories = array();
foreach ($this->listenerPositions as $position) {
$factories[$position] = array();
}
}
/**
* Returns the base path for the XSD files.
*
* @return string The XSD base path
*/
public function getXsdValidationBasePath()
{
return __DIR__.'/../Resources/config/schema';
}
foreach (array_keys($tags) as $tag) {
$factory = $c->get($tag);
$factories[$factory->getPosition()][] = $factory;
}
public function getNamespace()
{
return 'http://www.symfony-project.org/schema/dic/security';
}
public function getAlias()
{
return 'security';
return $this->factories = $factories;
}
}

View File

@ -28,12 +28,8 @@
<parameter key="security.authentication.listener.anonymous.class">Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener</parameter>
<parameter key="security.authentication.switchuser_listener.class">Symfony\Component\Security\Http\Firewall\SwitchUserListener</parameter>
<parameter key="security.authentication.switchuser.role">ROLE_ALLOWED_TO_SWITCH</parameter>
<parameter key="security.authentication.switchuser.parameter">_switch_user</parameter>
<parameter key="security.logout_listener.class">Symfony\Component\Security\Http\Firewall\LogoutListener</parameter>
<parameter key="security.logout.path">/logout</parameter>
<parameter key="security.logout.target_path">/</parameter>
<parameter key="security.logout.handler.session.class">Symfony\Component\Security\Http\Logout\SessionLogoutHandler</parameter>
<parameter key="security.logout.handler.cookie_clearing.class">Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler</parameter>
@ -83,14 +79,12 @@
<service id="security.context_listener" class="%security.context_listener.class%" public="false">
<argument type="service" id="security.context" />
<argument type="collection"></argument>
<argument />
<argument /> <!-- Provider Key -->
<argument type="service" id="logger" on-invalid="null" />
</service>
<service id="security.logout_listener" class="%security.logout_listener.class%" public="false" abstract="true">
<argument type="service" id="security.context" />
<argument>%security.logout.path%</argument>
<argument>%security.logout.target_path%</argument>
</service>
<service id="security.logout.handler.session" class="%security.logout.handler.session.class%" public="false" />
<service id="security.logout.handler.cookie_clearing" class="%security.logout.handler.cookie_clearing.class%" public="false" abstract="true" />
@ -162,13 +156,11 @@
<service id="security.authentication.switchuser_listener" class="%security.authentication.switchuser_listener.class%" public="false" abstract="true">
<argument type="service" id="security.context" />
<argument type="service" id="security.user.provider.in_memory" />
<argument /> <!-- User Provider -->
<argument type="service" id="security.account_checker" />
<argument /> <!-- Provider Key -->
<argument type="service" id="security.access.decision_manager" />
<argument type="service" id="logger" on-invalid="null" />
<argument>%security.authentication.switchuser.parameter%</argument>
<argument>%security.authentication.switchuser.role%</argument>
</service>
<service id="security.access_listener" class="%security.access_listener.class%" public="false">

View File

@ -0,0 +1,15 @@
<?php
$this->load('merge_import.php', $container);
$container->loadFromExtension('security', 'config', array(
'firewalls' => array(
'main' => array(
'form_login' => false,
'http_basic' => null,
),
),
'role_hierarchy' => array(
'FOO' => array('MOO'),
)
));

View File

@ -0,0 +1,15 @@
<?php
$container->loadFromExtension('security', 'config', array(
'firewalls' => array(
'main' => array(
'form_login' => array(
'login_path' => '/login',
)
)
),
'role_hierarchy' => array(
'FOO' => 'BAR',
'ADMIN' => 'USER',
),
));

View File

@ -6,12 +6,9 @@
xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">
<config>
<access-control>
<rule path="/blog/524" role="ROLE_USER" requires-channel="https" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY'>
<attribute key="_controller" pattern=".*\\BlogBundle\\.*" />
<path pattern="/blog/.*" />
</rule>
</access-control>
<rule path="/blog/524" role="ROLE_USER" requires-channel="https" />
<rule role='IS_AUTHENTICATED_ANONYMOUSLY' path="/blog/.*">
<attribute key="_controller" pattern=".*\\BlogBundle\\.*" />
</rule>
</config>
</srv:container>

View File

@ -6,10 +6,8 @@
xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">
<config>
<role-hierarchy>
<role id="ROLE_ADMIN">ROLE_USER</role>
<role id="ROLE_SUPER_ADMIN">ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH</role>
<role id="ROLE_REMOTE">ROLE_USER,ROLE_ADMIN</role>
</role-hierarchy>
<role id="ROLE_ADMIN">ROLE_USER</role>
<role id="ROLE_SUPER_ADMIN">ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH</role>
<role id="ROLE_REMOTE">ROLE_USER,ROLE_ADMIN</role>
</config>
</srv:container>

View File

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<container xmlns="http://www.symfony-project.org/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:sec="http://www.symfony-project.org/schema/dic/security"
xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">
<imports>
<import resource="merge_import.xml"/>
</imports>
<sec:config>
<sec:firewall name="main" form-login="false">
<sec:http-basic />
</sec:firewall>
<sec:role id="FOO" value="MOO" />
</sec:config>
</container>

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<srv:container xmlns="http://www.symfony-project.org/schema/dic/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:srv="http://www.symfony-project.org/schema/dic/services"
xsi:schemaLocation="http://www.symfony-project.org/schema/dic/services http://www.symfony-project.org/schema/dic/services/services-1.0.xsd">
<config>
<firewall name="main">
<form-login login-path="/login" />
</firewall>
<role id="FOO" value="BAR" />
<role id="ADMIN" value="USER" />
</config>
</srv:container>

View File

@ -0,0 +1,11 @@
imports:
- { resource: merge_import.yml }
security.config:
firewalls:
main:
form_login: false
http_basic: ~
role_hierarchy:
FOO: [MOO]

View File

@ -0,0 +1,9 @@
security.config:
firewalls:
main:
form_login:
login_path: /login
role_hierarchy:
FOO: BAR
ADMIN: USER

View File

@ -28,7 +28,7 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
list($authProviderId,
$listenerId,
$entryPointId
) = $factory->create($container, 'foo', array('use_forward' => true, 'failure_path' => '/foo', 'success_handler' => 'foo'), 'user_provider', 'entry_point');
) = $factory->create($container, 'foo', array('use_forward' => true, 'failure_path' => '/foo', 'success_handler' => 'foo', 'remember_me' => true), 'user_provider', 'entry_point');
// auth provider
$this->assertEquals('auth_provider', $authProviderId);
@ -41,15 +41,8 @@ class AbstractFactoryTest extends \PHPUnit_Framework_TestCase
$this->assertEquals(array(
'index_3' => 'foo',
'index_4' => array(
'check_path' => '/login_check',
'login_path' => '/login',
'use_forward' => true,
'always_use_default_target_path' => false,
'default_target_path' => '/',
'target_path_parameter' => '_target_path',
'use_referer' => false,
'failure_path' => '/foo',
'failure_forward' => false,
),
'index_5' => new Reference('foo'),
), $definition->getArguments());

View File

@ -36,10 +36,10 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase
$expectedProviders = array(
'security.authentication.provider.digest',
'security.authentication.provider.digest_0ff1b54f2a4b7f71b2b9d6604fcca4b8',
'security.authentication.provider.digest_23374fce51fe846516ff85bfa9add8fe',
'security.authentication.provider.basic',
'security.authentication.provider.basic_b7f0cf21802ffc8b22cadbb255f07213',
'security.authentication.provider.basic_98e44377704554700e68c22094b51ca4',
'security.authentication.provider.basic_745e8583f784c83c4b4208fd281001f3',
'security.authentication.provider.basic_af4bcce7246fb064b8e219034043d88a',
'security.authentication.provider.doctrine',
'security.authentication.provider.service',
'security.authentication.provider.anonymous',
@ -109,6 +109,16 @@ abstract class SecurityExtensionTest extends \PHPUnit_Framework_TestCase
}
}
public function testMerge()
{
$container = $this->getContainer('merge');
$this->assertEquals(array(
'FOO' => array('MOO'),
'ADMIN' => array('USER'),
), $container->getParameter('security.role_hierarchy.roles'));
}
protected function getContainer($file)
{
$container = new ContainerBuilder();

View File

@ -2,23 +2,117 @@
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Configuration\Exception\DuplicateKeyException;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidTypeException;
use Symfony\Component\DependencyInjection\Configuration\Exception\UnsetKeyException;
use Symfony\Component\DependencyInjection\Extension\Extension;
/**
* Represents an ARRAY node in the config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ArrayNode extends BaseNode implements PrototypeNodeInterface
{
protected $normalizeTransformations;
protected $xmlRemappings;
protected $children;
protected $prototype;
protected $keyAttribute;
protected $allowFalse;
protected $allowNewKeys;
protected $addIfNotSet;
protected $minNumberOfElements;
protected $performDeepMerging;
public function __construct($name, NodeInterface $parent = null, array $beforeTransformations = array(), array $afterTransformations = array(), array $normalizeTransformations = array(), $keyAttribute = null)
public function __construct($name, NodeInterface $parent = null)
{
parent::__construct($name, $parent, $beforeTransformations, $afterTransformations);
parent::__construct($name, $parent);
$this->children = array();
$this->normalizeTransformations = $normalizeTransformations;
$this->keyAttribute = $keyAttribute;
$this->xmlRemappings = array();
$this->allowFalse = false;
$this->addIfNotSet = false;
$this->allowNewKeys = true;
$this->performDeepMerging = true;
$this->minNumberOfElements = 0;
}
/**
* Sets the xml remappings that should be performed.
*
* @param array $remappings an array of the form array(array(string, string))
* @return void
*/
public function setXmlRemappings(array $remappings)
{
$this->xmlRemappings = $remappings;
}
/**
* Sets the minimum number of elements that a prototype based node must
* contain. By default this is zero, meaning no elements.
*
* @param integer $number
* @return void
*/
public function setMinNumberOfElements($number)
{
$this->minNumberOfElements = $number;
}
/**
* The name of the attribute that should be used as key.
*
* This is only relevant for XML configurations, and only in combination
* with a prototype based node.
*
* @param string $attribute
* @return void
*/
public function setKeyAttribute($attribute)
{
$this->keyAttribute = $attribute;
}
/**
* Sets whether to add default values for this array if it has not been
* defined in any of the configuration files.
*
* @param Boolean $boolean
* @return void
*/
public function setAddIfNotSet($boolean)
{
$this->addIfNotSet = (Boolean) $boolean;
}
/**
* Sets whether false is allowed as value indicating that the array should
* be unset.
*
* @param Boolean $allow
* @return void
*/
public function setAllowFalse($allow)
{
$this->allowFalse = (Boolean) $allow;
}
/**
* Sets whether new keys can be defined in subsequent configurations.
*
* @param Boolean $allow
* @return void
*/
public function setAllowNewKeys($allow)
{
$this->allowNewKeys = (Boolean) $allow;
}
public function setPerformDeepMerging($boolean)
{
$this->performDeepMerging = (Boolean) $boolean;
}
public function setName($name)
@ -26,10 +120,41 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
$this->name = $name;
}
public function hasDefaultValue()
{
if (null !== $this->prototype) {
return true;
}
return $this->addIfNotSet;
}
public function getDefaultValue()
{
if (!$this->hasDefaultValue()) {
throw new \RuntimeException(sprintf('The node at path "%s" has no default value.', $this->getPath()));
}
if (null !== $this->prototype) {
return array();
}
$defaults = array();
foreach ($this->children as $name => $child) {
if (!$child->hasDefaultValue()) {
continue;
}
$defaults[$name] = $child->getDefaultValue();
}
return $defaults;
}
public function setPrototype(PrototypeNodeInterface $node)
{
if (count($this->children) > 0) {
throw new \RuntimeException('An ARRAY node must either have concrete children, or a prototype node.');
throw new \RuntimeException($this->getPath().': An ARRAY node must either have concrete children, or a prototype node.');
}
$this->prototype = $node;
@ -51,9 +176,65 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
$this->children[$name] = $node;
}
protected function finalizeValue($value)
{
if (false === $value) {
throw new UnsetKeyException(sprintf(
'Unsetting key for path "%s", value: %s',
$this->getPath(),
json_encode($value)
));
}
if (null !== $this->prototype) {
foreach ($value as $k => $v) {
try {
$value[$k] = $this->prototype->finalize($v);
} catch (UnsetKeyException $unset) {
unset($value[$k]);
}
}
if (count($value) < $this->minNumberOfElements) {
throw new InvalidConfigurationException(sprintf(
'You must define at least %d element(s) for path "%s".',
$this->minNumberOfElements,
$this->getPath()
));
}
return $value;
}
foreach ($this->children as $name => $child) {
if (!array_key_exists($name, $value)) {
if ($child->isRequired()) {
throw new InvalidConfigurationException(sprintf(
'The node at path "%s" must be configured.',
$this->getPath()
));
}
if ($child->hasDefaultValue()) {
$value[$name] = $child->getDefaultValue();
}
continue;
}
try {
$value[$name] = $child->finalize($value[$name]);
} catch (UnsetKeyException $unset) {
unset($value[$name]);
}
}
return $value;
}
protected function validateType($value)
{
if (!is_array($value)) {
if (!is_array($value) && (!$this->allowFalse || false !== $value)) {
throw new InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected array, but got %s',
$this->getPath(),
@ -64,7 +245,11 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
protected function normalizeValue($value)
{
foreach ($this->normalizeTransformations as $transformation) {
if (false === $value) {
return $value;
}
foreach ($this->xmlRemappings as $transformation) {
list($singular, $plural) = $transformation;
if (!isset($value[$singular])) {
@ -77,8 +262,24 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
if (null !== $this->prototype) {
$normalized = array();
foreach ($value as $k => $v) {
if (null !== $this->keyAttribute && is_array($v) && isset($v[$this->keyAttribute])) {
$k = $v[$this->keyAttribute];
if (null !== $this->keyAttribute && is_array($v)) {
if (!isset($v[$this->keyAttribute]) && is_int($k)) {
throw new InvalidConfigurationException(sprintf(
'You must set a "%s" attribute for path "%s".',
$this->keyAttribute,
$this->getPath()
));
} else if (isset($v[$this->keyAttribute])) {
$k = $v[$this->keyAttribute];
}
if (array_key_exists($k, $normalized)) {
throw new DuplicateKeyException(sprintf(
'Duplicate key "%s" for path "%s".',
$k,
$this->getPath()
));
}
}
$this->prototype->setName($k);
@ -103,4 +304,50 @@ class ArrayNode extends BaseNode implements PrototypeNodeInterface
return $normalized;
}
}
protected function mergeValues($leftSide, $rightSide)
{
if (false === $rightSide) {
// if this is still false after the last config has been merged the
// finalization pass will take care of removing this key entirely
return false;
}
if (false === $leftSide || !$this->performDeepMerging) {
return $rightSide;
}
foreach ($rightSide as $k => $v) {
// no conflict
if (!array_key_exists($k, $leftSide)) {
if (!$this->allowNewKeys) {
throw new InvalidConfigurationException(sprintf(
'You are not allowed to define new elements for path "%s". '
.'Please define all elements for this path in one config file.',
$this->getPath()
));
}
$leftSide[$k] = $v;
continue;
}
try {
if (null !== $this->prototype) {
$this->prototype->setName($k);
$leftSide[$k] = $this->prototype->merge($leftSide[$k], $v);
} else {
if (!isset($this->children[$k])) {
throw new \RuntimeException('merge() expects a normalized config array.');
}
$leftSide[$k] = $this->children[$k]->merge($leftSide[$k], $v);
}
} catch (UnsetKeyException $unset) {
unset($leftSide[$k]);
}
}
return $leftSide;
}
}

View File

@ -2,15 +2,25 @@
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Exception\Exception;
use Symfony\Component\DependencyInjection\Configuration\Exception\ForbiddenOverwriteException;
/**
* The base node class
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
abstract class BaseNode implements NodeInterface
{
protected $name;
protected $parent;
protected $beforeTransformations;
protected $afterTransformations;
protected $nodeFactory;
protected $normalizationClosures;
protected $finalValidationClosures;
protected $allowOverwrite;
protected $required;
protected $equivalentValues;
public function __construct($name, NodeInterface $parent = null, $beforeTransformations = array(), $afterTransformations = array())
public function __construct($name, NodeInterface $parent = null)
{
if (false !== strpos($name, '.')) {
throw new \InvalidArgumentException('The name must not contain ".".');
@ -18,8 +28,41 @@ abstract class BaseNode implements NodeInterface
$this->name = $name;
$this->parent = $parent;
$this->beforeTransformations = $beforeTransformations;
$this->afterTransformations = $afterTransformations;
$this->normalizationClosures = array();
$this->finalValidationClosures = array();
$this->allowOverwrite = true;
$this->required = false;
$this->equivalentValues = array();
}
public function addEquivalentValue($originalValue, $equivalentValue)
{
$this->equivalentValues[] = array($originalValue, $equivalentValue);
}
public function setRequired($boolean)
{
$this->required = (Boolean) $boolean;
}
public function setAllowOverwrite($allow)
{
$this->allowOverwrite = (Boolean) $allow;
}
public function setNormalizationClosures(array $closures)
{
$this->normalizationClosures = $closures;
}
public function setFinalValidationClosures(array $closures)
{
$this->finalValidationClosures = $closures;
}
public function isRequired()
{
return $this->required;
}
public function getName()
@ -38,22 +81,64 @@ abstract class BaseNode implements NodeInterface
return $path;
}
public final function merge($leftSide, $rightSide)
{
if (!$this->allowOverwrite) {
throw new ForbiddenOverwriteException(sprintf(
'Configuration path "%s" cannot be overwritten. You have to '
.'define all options for this path, and any of its sub-paths in '
.'one configuration section.',
$this->getPath()
));
}
$this->validateType($leftSide);
$this->validateType($rightSide);
return $this->mergeValues($leftSide, $rightSide);
}
public final function normalize($value)
{
// run before transformations
foreach ($this->beforeTransformations as $transformation) {
$value = $transformation($value);
// run custom normalization closures
foreach ($this->normalizationClosures as $closure) {
$value = $closure($value);
}
// replace value with their equivalent
foreach ($this->equivalentValues as $data) {
if ($data[0] === $value) {
$value = $data[1];
}
}
// validate type
$this->validateType($value);
// normalize value
$value = $this->normalizeValue($value);
return $this->normalizeValue($value);
}
// run after transformations
foreach ($this->afterTransformations as $transformation) {
$value = $transformation($value);
public final function finalize($value)
{
$this->validateType($value);
$value = $this->finalizeValue($value);
// Perform validation on the final value if a closure has been set.
// The closure is also allowed to return another value.
foreach ($this->finalValidationClosures as $closure) {
try {
$value = $closure($value);
} catch (Exception $correctEx) {
throw $correctEx;
} catch (\Exception $invalid) {
throw new InvalidConfigurationException(sprintf(
'Invalid configuration for path "%s": %s',
$this->getPath(),
$invalid->getMessage()
), $invalid->getCode(), $invalid);
}
}
return $value;
@ -61,4 +146,6 @@ abstract class BaseNode implements NodeInterface
abstract protected function validateType($value);
abstract protected function normalizeValue($value);
abstract protected function mergeValues($leftSide, $rightSide);
abstract protected function finalizeValue($value);
}

View File

@ -0,0 +1,21 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidTypeException;
class BooleanNode extends ScalarNode
{
protected function validateType($value)
{
parent::validateType($value);
if (!is_bool($value)) {
throw new InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected boolean, but got %s.',
$this->getPath(),
json_encode($value)
));
}
}
}

View File

@ -2,6 +2,11 @@
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
/**
* This class builds an if expression.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ExprBuilder
{
public $parent;
@ -13,8 +18,12 @@ class ExprBuilder
$this->parent = $parent;
}
public function ifTrue(\Closure $closure)
public function ifTrue(\Closure $closure = null)
{
if (null === $closure) {
$closure = function($v) { return true === $v; };
}
$this->ifPart = $closure;
return $this;
@ -48,6 +57,31 @@ class ExprBuilder
return $this;
}
public function thenReplaceKeyWithAttribute($attribute)
{
$this->thenPart = function($v) {
$newValue = array();
foreach ($v as $k => $oldValue) {
if (is_array($oldValue) && isset($oldValue['id'])) {
$k = $oldValue['id'];
}
$newValue[$k] = $oldValue;
}
return $newValue;
};
return $this;
}
public function thenEmptyArray()
{
$this->thenPart = function($v) { return array(); };
return $this;
}
public function end()
{
if (null === $this->ifPart) {

View File

@ -0,0 +1,41 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
/**
* This class builds merge conditions.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class MergeBuilder
{
public $parent;
public $allowFalse;
public $allowOverwrite;
public function __construct($parent)
{
$this->parent = $parent;
$this->allowFalse = false;
$this->allowOverwrite = true;
}
public function allowUnset($allow = true)
{
$this->allowFalse = $allow;
return $this;
}
public function denyOverwrite($deny = true)
{
$this->allowOverwrite = !$deny;
return $this;
}
public function end()
{
return $this->parent;
}
}

View File

@ -2,6 +2,11 @@
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
/**
* This class provides a fluent interface for building a config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class NodeBuilder
{
/************
@ -13,9 +18,20 @@ class NodeBuilder
public $parent;
public $children;
public $prototype;
public $normalizeTransformations;
public $beforeTransformations;
public $afterTransformations;
public $normalization;
public $merge;
public $finalization;
public $defaultValue;
public $default;
public $addDefaults;
public $required;
public $atLeastOne;
public $allowNewKeys;
public $allowEmptyValue;
public $nullEquivalent;
public $trueEquivalent;
public $falseEquivalent;
public $performDeepMerging;
public function __construct($name, $type, $parent = null)
{
@ -23,10 +39,28 @@ class NodeBuilder
$this->type = $type;
$this->parent = $parent;
$this->children =
$this->beforeTransformations =
$this->afterTransformations =
$this->normalizeTransformations = array();
$this->default = false;
$this->required = false;
$this->addDefaults = false;
$this->allowNewKeys = true;
$this->atLeastOne = false;
$this->allowEmptyValue = true;
$this->children = array();
$this->performDeepMerging = true;
if ('boolean' === $type) {
$this->nullEquivalent = true;
} else if ('array' === $type) {
$this->nullEquivalent = array();
}
if ('array' === $type) {
$this->trueEquivalent = array();
} else {
$this->trueEquivalent = true;
}
$this->falseEquivalent = false;
}
/****************************
@ -35,54 +69,178 @@ class NodeBuilder
public function node($name, $type)
{
$node = new NodeBuilder($name, $type, $this);
$node = new static($name, $type, $this);
return $this->children[$name] = $node;
}
public function normalize($key, $plural = null)
public function arrayNode($name)
{
if (null === $plural) {
$plural = $key.'s';
}
return $this->node($name, 'array');
}
$this->normalizeTransformations[] = array($key, $plural);
public function scalarNode($name)
{
return $this->node($name, 'scalar');
}
public function booleanNode($name)
{
return $this->node($name, 'boolean');
}
public function defaultValue($value)
{
$this->default = true;
$this->defaultValue = $value;
return $this;
}
public function key($name)
public function isRequired()
{
$this->required = true;
return $this;
}
public function containsNameValuePairsWithKeyAttribute($attribute)
{
$this->beforeNormalization()
->ifArray()
->thenReplaceKeyWithAttribute($attribute)
;
$this->useAttributeAsKey($attribute);
return $this;
}
public function requiresAtLeastOneElement()
{
$this->atLeastOne = true;
return $this;
}
public function treatNullLike($value)
{
$this->nullEquivalent = $value;
return $this;
}
public function treatTrueLike($value)
{
$this->trueEquivalent = $value;
return $this;
}
public function treatFalseLike($value)
{
$this->falseEquivalent = $value;
return $this;
}
public function defaultNull()
{
return $this->defaultValue(null);
}
public function defaultTrue()
{
return $this->defaultValue(true);
}
public function defaultFalse()
{
return $this->defaultValue(false);
}
public function addDefaultsIfNotSet()
{
$this->addDefaults = true;
return $this;
}
public function disallowNewKeysInSubsequentConfigs()
{
$this->allowNewKeys = false;
return $this;
}
protected function normalization()
{
if (null === $this->normalization) {
$this->normalization = new NormalizationBuilder($this);
}
return $this->normalization;
}
public function beforeNormalization()
{
return $this->normalization()->before();
}
public function fixXmlConfig($singular, $plural = null)
{
$this->normalization()->remap($singular, $plural);
return $this;
}
public function useAttributeAsKey($name)
{
$this->key = $name;
return $this;
}
public function before(\Closure $closure = null)
protected function merge()
{
if (null !== $closure) {
$this->beforeTransformations[] = $closure;
return $this;
if (null === $this->merge) {
$this->merge = new MergeBuilder($this);
}
return $this->beforeTransformations[] = new ExprBuilder($this);
return $this->merge;
}
public function cannotBeOverwritten($deny = true)
{
$this->merge()->denyOverwrite($deny);
return $this;
}
public function cannotBeEmpty()
{
$this->allowEmptyValue = false;
return $this;
}
public function canBeUnset($allow = true)
{
$this->merge()->allowUnset($allow);
return $this;
}
public function prototype($type)
{
return $this->prototype = new NodeBuilder(null, $type, $this);
return $this->prototype = new static(null, $type, $this);
}
public function after(\Closure $closure = null)
public function performNoDeepMerging()
{
if (null !== $closure) {
$this->afterTransformations[] = $closure;
$this->performDeepMerging = false;
return $this;
}
return $this->afterTransformations[] = new ExprBuilder($this);
return $this;
}
public function end()

View File

@ -0,0 +1,48 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
/**
* This class builds normalization conditions.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class NormalizationBuilder
{
public $parent;
public $before;
public $remappings;
public function __construct($parent)
{
$this->parent = $parent;
$this->keys = false;
$this->remappings =
$this->before =
$this->after = array();
}
public function remap($key, $plural = null)
{
if (null === $plural) {
$plural = $key.'s';
}
$this->remappings[] = array($key, $plural);
return $this;
}
public function before(\Closure $closure = null)
{
if (null !== $closure) {
$this->before[] = $closure;
return $this;
}
return $this->before[] = new ExprBuilder($this->parent);
}
}

View File

@ -2,9 +2,18 @@
namespace Symfony\Component\DependencyInjection\Configuration\Builder;
use Symfony\Component\DependencyInjection\Configuration\BaseNode;
use Symfony\Component\DependencyInjection\Configuration\BooleanNode;
use Symfony\Component\DependencyInjection\Configuration\ArrayNode;
use Symfony\Component\DependencyInjection\Configuration\ScalarNode;
/**
* This is the entry class for building your own config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class TreeBuilder
{
protected $root;
@ -32,9 +41,6 @@ class TreeBuilder
protected function createConfigNode(NodeBuilder $node)
{
$node->beforeTransformations = $this->buildExpressions($node->beforeTransformations);
$node->afterTransformations = $this->buildExpressions($node->afterTransformations);
$method = 'create'.$node->type.'ConfigNode';
if (!method_exists($this, $method)) {
throw new \RuntimeException(sprintf('Unknown node type: "%s"', $node->type));
@ -43,14 +49,77 @@ class TreeBuilder
return $this->$method($node);
}
protected function createBooleanConfigNode(NodeBuilder $node)
{
$configNode = new BooleanNode($node->name, $node->parent);
$this->configureScalarNode($configNode, $node);
return $configNode;
}
protected function createScalarConfigNode(NodeBuilder $node)
{
return new ScalarNode($node->name, $node->parent, $node->beforeTransformations, $node->afterTransformations);
$configNode = new ScalarNode($node->name, $node->parent);
$this->configureScalarNode($configNode, $node);
return $configNode;
}
protected function configureScalarNode(ScalarNode $configNode, NodeBuilder $node)
{
if (null !== $node->normalization) {
$configNode->setNormalizationClosures(
$this->buildExpressions($node->normalization->before)
);
}
if (null !== $node->merge) {
$configNode->setAllowOverwrite($node->merge->allowOverwrite);
}
if (true === $node->default) {
$configNode->setDefaultValue($node->defaultValue);
}
if (false === $node->allowEmptyValue) {
$configNode->setAllowEmptyValue($node->allowEmptyValue);
}
$configNode->addEquivalentValue(null, $node->nullEquivalent);
$configNode->addEquivalentValue(true, $node->trueEquivalent);
$configNode->addEquivalentValue(false, $node->falseEquivalent);
}
protected function createArrayConfigNode(NodeBuilder $node)
{
$configNode = new ArrayNode($node->name, $node->parent, $node->beforeTransformations, $node->afterTransformations, $node->normalizeTransformations, $node->key);
$configNode = new ArrayNode($node->name, $node->parent);
$configNode->setAddIfNotSet($node->addDefaults);
$configNode->setAllowNewKeys($node->allowNewKeys);
$configNode->addEquivalentValue(null, $node->nullEquivalent);
$configNode->addEquivalentValue(true, $node->trueEquivalent);
$configNode->addEquivalentValue(false, $node->falseEquivalent);
$configNode->setPerformDeepMerging($node->performDeepMerging);
if (null !== $node->key) {
$configNode->setKeyAttribute($node->key);
}
if (true === $node->atLeastOne) {
$configNode->setMinNumberOfElements(1);
}
if (null !== $node->normalization) {
$configNode->setNormalizationClosures(
$this->buildExpressions($node->normalization->before)
);
$configNode->setXmlRemappings($node->normalization->remappings);
}
if (null !== $node->merge) {
$configNode->setAllowOverwrite($node->merge->allowOverwrite);
$configNode->setAllowFalse($node->merge->allowFalse);
}
foreach ($node->children as $child) {
$child->parent = $configNode;

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* This exception is thrown whenever the key of an array is not unique. This can
* only be the case if the configuration is coming from an XML file.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class DuplicateKeyException extends InvalidConfigurationException
{
}

View File

@ -2,6 +2,11 @@
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* Base exception for all configuration exceptions
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Exception extends \RuntimeException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* This exception is thrown when a configuration path is overwritten from a
* subsequent configuration file, but the entry node specifically forbids this.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ForbiddenOverwriteException extends InvalidConfigurationException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* A very general exception which can be thrown whenever non of the more specific
* exceptions is suitable.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InvalidConfigurationException extends Exception
{
}

View File

@ -5,8 +5,8 @@ namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* This exception is thrown if an invalid type is encountered.
*
* @author johannes
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class InvalidTypeException extends Exception
class InvalidTypeException extends InvalidConfigurationException
{
}

View File

@ -0,0 +1,13 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration\Exception;
/**
* This exception is usually not encountered by the end-user, but only used
* internally to signal the parent scope to unset a key.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class UnsetKeyException extends Exception
{
}

View File

@ -2,9 +2,21 @@
namespace Symfony\Component\DependencyInjection\Configuration;
/**
* Common Interface among all nodes.
*
* In most cases, it is better to inherit from BaseNode instead of implementing
* this interface yourself.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
interface NodeInterface
{
function getName();
function getPath();
function isRequired();
function hasDefaultValue();
function getDefaultValue();
function normalize($value);
function merge($leftSide, $rightSide);
}

View File

@ -0,0 +1,26 @@
<?php
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Extension\Extension;
/**
* This class is the entry point for config normalization/merging/finalization.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class Processor
{
public function process(NodeInterface $configTree, array $configs)
{
$configs = Extension::normalizeKeys($configs);
$currentConfig = array();
foreach ($configs as $config) {
$config = $configTree->normalize($config);
$currentConfig = $configTree->merge($currentConfig, $config);
}
return $configTree->finalize($currentConfig);
}
}

View File

@ -2,10 +2,41 @@
namespace Symfony\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidConfigurationException;
use Symfony\Component\DependencyInjection\Configuration\Exception\InvalidTypeException;
/**
* This node represents a scalar value in the config tree.
*
* @author Johannes M. Schmitt <schmittjoh@gmail.com>
*/
class ScalarNode extends BaseNode implements PrototypeNodeInterface
{
protected $defaultValueSet = false;
protected $defaultValue;
protected $allowEmptyValue = true;
public function setDefaultValue($value)
{
$this->defaultValueSet = true;
$this->defaultValue = $value;
}
public function hasDefaultValue()
{
return $this->defaultValueSet;
}
public function getDefaultValue()
{
return $this->defaultValue;
}
public function setAllowEmptyValue($boolean)
{
$this->allowEmptyValue = (Boolean) $boolean;
}
public function setName($name)
{
$this->name = $name;
@ -14,7 +45,7 @@ class ScalarNode extends BaseNode implements PrototypeNodeInterface
protected function validateType($value)
{
if (!is_scalar($value)) {
throw new \InvalidTypeException(sprintf(
throw new InvalidTypeException(sprintf(
'Invalid type for path "%s". Expected scalar, but got %s.',
$this->getPath(),
json_encode($value)
@ -22,8 +53,26 @@ class ScalarNode extends BaseNode implements PrototypeNodeInterface
}
}
protected function finalizeValue($value)
{
if (!$this->allowEmptyValue && empty($value)) {
throw new InvalidConfigurationException(sprintf(
'The path "%s" cannot contain an empty value, but got %s.',
$this->getPath(),
json_encode($value)
));
}
return $value;
}
protected function normalizeValue($value)
{
return $value;
}
protected function mergeValues($leftSide, $rightSide)
{
return $rightSide;
}
}

View File

@ -0,0 +1,17 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\ArrayNode;
class ArrayNodeTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException Symfony\Component\DependencyInjection\Configuration\Exception\InvalidTypeException
*/
public function testNormalizeThrowsExceptionWhenFalseIsNotAllowed()
{
$node = new ArrayNode('root');
$node->normalize(false);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
use Symfony\Component\DependencyInjection\Configuration\Processor;
use Symfony\Component\DependencyInjection\Configuration\NodeInterface;
class FinalizationTest extends \PHPUnit_Framework_TestCase
{
public function testUnsetKeyWithDeepHierarchy()
{
$tb = new TreeBuilder();
$tree = $tb
->root('config', 'array')
->node('level1', 'array')
->canBeUnset()
->node('level2', 'array')
->canBeUnset()
->node('somevalue', 'scalar')->end()
->node('anothervalue', 'scalar')->end()
->end()
->node('level1_scalar', 'scalar')->end()
->end()
->end()
->buildTree()
;
$a = array(
'level1' => array(
'level2' => array(
'somevalue' => 'foo',
'anothervalue' => 'bar',
),
'level1_scalar' => 'foo',
),
);
$b = array(
'level1' => array(
'level2' => false,
),
);
$this->assertEquals(array(
'level1' => array(
'level1_scalar' => 'foo',
),
), $this->process($tree, array($a, $b)));
}
protected function process(NodeInterface $tree, array $configs)
{
$processor = new Processor();
return $processor->process($tree, $configs);
}
}

View File

@ -0,0 +1,148 @@
<?php
namespace Symfony\Tests\Component\DependencyInjection\Configuration;
use Symfony\Component\DependencyInjection\Configuration\Builder\TreeBuilder;
class MergeTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException Symfony\Component\DependencyInjection\Configuration\Exception\ForbiddenOverwriteException
*/
public function testForbiddenOverwrite()
{
$tb = new TreeBuilder();
$tree = $tb
->root('root', 'array')
->node('foo', 'scalar')
->merge()
->denyOverwrite()
->end()
->end()
->end()
->buildTree()
;
$a = array(
'foo' => 'bar',
);
$b = array(
'foo' => 'moo',
);
$tree->merge($a, $b);
}
public function testUnsetKey()
{
$tb = new TreeBuilder();
$tree = $tb
->root('root', 'array')
->node('foo', 'scalar')->end()
->node('bar', 'scalar')->end()
->node('unsettable', 'array')
->merge()->allowUnset()->end()
->node('foo', 'scalar')->end()
->node('bar', 'scalar')->end()
->end()
->node('unsetted', 'array')
->merge()->allowUnset()->end()
->prototype('scalar')->end()
->end()
->end()
->buildTree()
;
$a = array(
'foo' => 'bar',
'unsettable' => array(
'foo' => 'a',
'bar' => 'b',
),
'unsetted' => false,
);
$b = array(
'foo' => 'moo',
'bar' => 'b',
'unsettable' => false,
'unsetted' => array('a', 'b'),
);
$this->assertEquals(array(
'foo' => 'moo',
'bar' => 'b',
'unsettable' => false,
'unsetted' => array('a', 'b'),
), $tree->merge($a, $b));
}
/**
* @expectedException Symfony\Component\DependencyInjection\Configuration\Exception\InvalidConfigurationException
*/
public function testDoesNotAllowNewKeysInSubsequentConfigs()
{
$tb = new TreeBuilder();
$tree = $tb
->root('config', 'array')
->node('test', 'array')
->disallowNewKeysInSubsequentConfigs()
->useAttributeAsKey('key')
->prototype('array')
->node('value', 'scalar')->end()
->end()
->end()
->end()
->buildTree();
$a = array(
'test' => array(
'a' => array('value' => 'foo')
)
);
$b = array(
'test' => array(
'b' => array('value' => 'foo')
)
);
$tree->merge($a, $b);
}
public function testPerformsNoDeepMerging()
{
$tb = new TreeBuilder();
$tree = $tb
->root('config', 'array')
->node('no_deep_merging', 'array')
->performNoDeepMerging()
->node('foo', 'scalar')->end()
->node('bar', 'scalar')->end()
->end()
->end()
->buildTree()
;
$a = array(
'no_deep_merging' => array(
'foo' => 'a',
'bar' => 'b',
),
);
$b = array(
'no_deep_merging' => array(
'c' => 'd',
)
);
$this->assertEquals(array(
'no_deep_merging' => array(
'c' => 'd',
)
), $tree->merge($a, $b));
}
}

View File

@ -15,11 +15,11 @@ class NormalizerTest extends \PHPUnit_Framework_TestCase
$tb = new TreeBuilder();
$tree = $tb
->root('root_name', 'array')
->normalize('encoder')
->fixXmlConfig('encoder')
->node('encoders', 'array')
->key('class')
->useAttributeAsKey('class')
->prototype('array')
->before()->ifString()->then(function($v) { return array('algorithm' => $v); })->end()
->beforeNormalization()->ifString()->then(function($v) { return array('algorithm' => $v); })->end()
->node('algorithm', 'scalar')->end()
->end()
->end()
@ -87,7 +87,7 @@ class NormalizerTest extends \PHPUnit_Framework_TestCase
$tree = $tb
->root('root', 'array')
->node('logout', 'array')
->normalize('handler')
->fixXmlConfig('handler')
->node('handlers', 'array')
->prototype('scalar')->end()
->end()