diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Configuration.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000000..5f48157cb3 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Configuration.php @@ -0,0 +1,234 @@ + + */ +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() + ; + } +} \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php index 860ba9e145..88b481e3b1 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/AbstractFactory.php @@ -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; - } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php index 6149361fb8..eef9aecdc4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginFactory.php @@ -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; diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php index 3686a1e06f..9b2b7a870a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicFactory.php @@ -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() + ; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php index d837032e41..49cf748cf4 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpDigestFactory.php @@ -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() + ; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php index e8646e90d0..c4727429ab 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/RememberMeFactory.php @@ -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); + } + } + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php index a2ec07df05..05dcc74f8a 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/SecurityFactoryInterface.php @@ -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); } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php index dbf0e35914..d53f75d978 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/X509Factory.php @@ -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() + ; + } } diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php index edc27e69f2..ac835082bd 100644 --- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/SecurityExtension.php @@ -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 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; } } diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 3245168813..f36a6c461a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -28,12 +28,8 @@ Symfony\Component\Security\Http\Firewall\AnonymousAuthenticationListener Symfony\Component\Security\Http\Firewall\SwitchUserListener - ROLE_ALLOWED_TO_SWITCH - _switch_user Symfony\Component\Security\Http\Firewall\LogoutListener - /logout - / Symfony\Component\Security\Http\Logout\SessionLogoutHandler Symfony\Component\Security\Http\Logout\CookieClearingLogoutHandler @@ -83,14 +79,12 @@ - + - %security.logout.path% - %security.logout.target_path% @@ -162,13 +156,11 @@ - + - %security.authentication.switchuser.parameter% - %security.authentication.switchuser.role% diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php new file mode 100644 index 0000000000..988640fb28 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge.php @@ -0,0 +1,15 @@ +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'), + ) +)); \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php new file mode 100644 index 0000000000..2b9be399d7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/php/merge_import.php @@ -0,0 +1,15 @@ +loadFromExtension('security', 'config', array( + 'firewalls' => array( + 'main' => array( + 'form_login' => array( + 'login_path' => '/login', + ) + ) + ), + 'role_hierarchy' => array( + 'FOO' => 'BAR', + 'ADMIN' => 'USER', + ), +)); \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access.xml index c01acd6101..2ca9f5b9d0 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/access.xml @@ -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"> - - - - - - - + + + + diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/hierarchy.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/hierarchy.xml index 795105230c..4c8985a791 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/hierarchy.xml +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/hierarchy.xml @@ -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"> - - ROLE_USER - ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH - ROLE_USER,ROLE_ADMIN - + ROLE_USER + ROLE_USER,ROLE_ADMIN,ROLE_ALLOWED_TO_SWITCH + ROLE_USER,ROLE_ADMIN diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml new file mode 100644 index 0000000000..36f7b4de72 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml new file mode 100644 index 0000000000..806719ab5f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/xml/merge_import.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml new file mode 100644 index 0000000000..a42fc99fab --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge.yml @@ -0,0 +1,11 @@ +imports: + - { resource: merge_import.yml } + +security.config: + firewalls: + main: + form_login: false + http_basic: ~ + + role_hierarchy: + FOO: [MOO] \ No newline at end of file diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml new file mode 100644 index 0000000000..497fb398c7 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Fixtures/yml/merge_import.yml @@ -0,0 +1,9 @@ +security.config: + firewalls: + main: + form_login: + login_path: /login + + role_hierarchy: + FOO: BAR + ADMIN: USER diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php index 9d6d1390ee..c412ce0e30 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/Security/Factory/AbstractFactoryTest.php @@ -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()); diff --git a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php index d76f505d93..73df009691 100644 --- a/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php +++ b/src/Symfony/Bundle/SecurityBundle/Tests/DependencyInjection/SecurityExtensionTest.php @@ -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(); diff --git a/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php b/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php index 9a9fc165f8..1b9cbdfdc1 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/ArrayNode.php @@ -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 + */ 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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php b/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php index 69f5847fbc..b2a78e9e91 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/BaseNode.php @@ -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 + */ 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); } \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/BooleanNode.php b/src/Symfony/Component/DependencyInjection/Configuration/BooleanNode.php new file mode 100644 index 0000000000..3960e0e756 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/BooleanNode.php @@ -0,0 +1,21 @@ +getPath(), + json_encode($value) + )); + } + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php index c8f2e0cbdd..1e29a6ed72 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/ExprBuilder.php @@ -2,6 +2,11 @@ namespace Symfony\Component\DependencyInjection\Configuration\Builder; +/** + * This class builds an if expression. + * + * @author Johannes M. Schmitt + */ 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) { diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/MergeBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/MergeBuilder.php new file mode 100644 index 0000000000..5ac10001db --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/MergeBuilder.php @@ -0,0 +1,41 @@ + + */ +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; + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php index 0c63068d71..aa19de0e61 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NodeBuilder.php @@ -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 + */ 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() diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/NormalizationBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NormalizationBuilder.php new file mode 100644 index 0000000000..96e76cb586 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/NormalizationBuilder.php @@ -0,0 +1,48 @@ + + */ +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); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php b/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php index 93f48bdf19..e24a23cb4d 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/Builder/TreeBuilder.php @@ -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 + */ 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; diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/DuplicateKeyException.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/DuplicateKeyException.php new file mode 100644 index 0000000000..7da500ba56 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/DuplicateKeyException.php @@ -0,0 +1,13 @@ + + */ +class DuplicateKeyException extends InvalidConfigurationException +{ +} diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php index e5a464b276..c669089f47 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/Exception.php @@ -2,6 +2,11 @@ namespace Symfony\Component\DependencyInjection\Configuration\Exception; +/** + * Base exception for all configuration exceptions + * + * @author Johannes M. Schmitt + */ class Exception extends \RuntimeException { } \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/ForbiddenOverwriteException.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/ForbiddenOverwriteException.php new file mode 100644 index 0000000000..0f7537747e --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/ForbiddenOverwriteException.php @@ -0,0 +1,13 @@ + + */ +class ForbiddenOverwriteException extends InvalidConfigurationException +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidConfigurationException.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidConfigurationException.php new file mode 100644 index 0000000000..71f3ffbd52 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidConfigurationException.php @@ -0,0 +1,13 @@ + + */ +class InvalidConfigurationException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidTypeException.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidTypeException.php index 436c80fa80..3cdbc52439 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidTypeException.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/InvalidTypeException.php @@ -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 */ -class InvalidTypeException extends Exception +class InvalidTypeException extends InvalidConfigurationException { } \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Exception/UnsetKeyException.php b/src/Symfony/Component/DependencyInjection/Configuration/Exception/UnsetKeyException.php new file mode 100644 index 0000000000..2388b134b4 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Exception/UnsetKeyException.php @@ -0,0 +1,13 @@ + + */ +class UnsetKeyException extends Exception +{ +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/NodeInterface.php b/src/Symfony/Component/DependencyInjection/Configuration/NodeInterface.php index a5e8611c63..70271946b4 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/NodeInterface.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/NodeInterface.php @@ -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 + */ interface NodeInterface { function getName(); function getPath(); + function isRequired(); + function hasDefaultValue(); + function getDefaultValue(); function normalize($value); + function merge($leftSide, $rightSide); } \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/Processor.php b/src/Symfony/Component/DependencyInjection/Configuration/Processor.php new file mode 100644 index 0000000000..cdabd29a03 --- /dev/null +++ b/src/Symfony/Component/DependencyInjection/Configuration/Processor.php @@ -0,0 +1,26 @@ + + */ +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); + } +} \ No newline at end of file diff --git a/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php b/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php index cdcebe3dac..fd871c3864 100644 --- a/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php +++ b/src/Symfony/Component/DependencyInjection/Configuration/ScalarNode.php @@ -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 + */ 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; + } } \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/ArrayNodeTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/ArrayNodeTest.php new file mode 100644 index 0000000000..da3155493e --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/ArrayNodeTest.php @@ -0,0 +1,17 @@ +normalize(false); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/FinalizationTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/FinalizationTest.php new file mode 100644 index 0000000000..ab3f5af12b --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/FinalizationTest.php @@ -0,0 +1,59 @@ +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); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/MergeTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/MergeTest.php new file mode 100644 index 0000000000..0fab77cc11 --- /dev/null +++ b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/MergeTest.php @@ -0,0 +1,148 @@ +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)); + } +} \ No newline at end of file diff --git a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php index 4afbdb9eee..9254700eb6 100644 --- a/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php +++ b/tests/Symfony/Tests/Component/DependencyInjection/Configuration/NormalizationTest.php @@ -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()