From b8291ccce6b3d862c95c00dc0b7efb852144efcc Mon Sep 17 00:00:00 2001 From: Thomas Lallement Date: Thu, 21 Apr 2022 12:33:21 +0200 Subject: [PATCH 1/2] Split configuration For better readability and to fix https://github.com/symfony/monolog-bundle/issues/412 As proposed here https://github.com/symfony/monolog-bundle/issues/412#issuecomment-1091780515 Workarround for PHP issue php/php-src#8315 --- DependencyInjection/Configuration.php | 1301 +++++++++++++------------ 1 file changed, 689 insertions(+), 612 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 86c9f870..e091ba16 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -12,6 +12,7 @@ namespace Symfony\Bundle\MonologBundle\DependencyInjection; use Symfony\Component\Config\Definition\BaseNode; +use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException; @@ -355,7 +356,7 @@ public function getConfigTreeBuilder() $treeBuilder = new TreeBuilder('monolog'); $rootNode = method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('monolog'); - $rootNode + $handlerNode = $rootNode ->fixXmlConfig('channel') ->fixXmlConfig('handler') ->children() @@ -374,639 +375,715 @@ public function getConfigTreeBuilder() ->fixXmlConfig('tag') ->fixXmlConfig('accepted_level') ->fixXmlConfig('header') - ->canBeUnset() - ->children() - ->scalarNode('type') - ->isRequired() - ->treatNullLike('null') - ->beforeNormalization() - ->always() - ->then(function ($v) { return strtolower($v); }) - ->end() - ->end() - ->scalarNode('id')->end() // service & rollbar - ->scalarNode('priority')->defaultValue(0)->end() - ->scalarNode('level')->defaultValue('DEBUG')->end() - ->booleanNode('bubble')->defaultTrue()->end() - ->scalarNode('app_name')->defaultNull()->end() - ->booleanNode('include_stacktraces')->defaultFalse()->end() - ->booleanNode('process_psr_3_messages')->defaultNull()->end() - ->scalarNode('path')->defaultValue('%kernel.logs_dir%/%kernel.environment%.log')->end() // stream and rotating - ->scalarNode('file_permission') // stream and rotating - ->defaultNull() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { - if (substr($v, 0, 1) === '0') { - return octdec($v); - } + ->canBeUnset(); - return (int) $v; - }) - ->end() - ->end() - ->booleanNode('use_locking')->defaultFalse()->end() // stream and rotating - ->scalarNode('filename_format')->defaultValue('{filename}-{date}')->end() //rotating - ->scalarNode('date_format')->defaultValue('Y-m-d')->end() //rotating - ->scalarNode('ident')->defaultFalse()->end() // syslog and syslogudp - ->scalarNode('logopts')->defaultValue(LOG_PID)->end() // syslog - ->scalarNode('facility')->defaultValue('user')->end() // syslog - ->scalarNode('max_files')->defaultValue(0)->end() // rotating - ->scalarNode('action_level')->defaultValue('WARNING')->end() // fingers_crossed - ->scalarNode('activation_strategy')->defaultNull()->end() // fingers_crossed - ->booleanNode('stop_buffering')->defaultTrue()->end()// fingers_crossed - ->scalarNode('passthru_level')->defaultNull()->end() // fingers_crossed - ->arrayNode('excluded_404s') // fingers_crossed - ->canBeUnset() - ->prototype('scalar')->end() - ->end() - ->arrayNode('excluded_http_codes') // fingers_crossed - ->canBeUnset() - ->beforeNormalization() - ->always(function ($values) { - return array_map(function ($value) { - /* - * Allows YAML: - * excluded_http_codes: [403, 404, { 400: ['^/foo', '^/bar'] }] - * - * and XML: - * - * ^/foo - * ^/bar - * - * - */ + $handlerNode + ->children() + ->scalarNode('type') + ->isRequired() + ->treatNullLike('null') + ->beforeNormalization() + ->always() + ->then(function ($v) { return strtolower($v); }) + ->end() + ->end() + ->scalarNode('id')->end() // service & rollbar + ->scalarNode('priority')->defaultValue(0)->end() + ->scalarNode('level')->defaultValue('DEBUG')->end() + ->booleanNode('bubble')->defaultTrue()->end() + ->scalarNode('app_name')->defaultNull()->end() + ->booleanNode('include_stacktraces')->defaultFalse()->end() + ->booleanNode('process_psr_3_messages')->defaultNull()->end() + ->scalarNode('path')->defaultValue('%kernel.logs_dir%/%kernel.environment%.log')->end() // stream and rotating + ->scalarNode('file_permission') // stream and rotating + ->defaultNull() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { + if (substr($v, 0, 1) === '0') { + return octdec($v); + } - if (is_array($value)) { - return isset($value['code']) ? $value : ['code' => key($value), 'urls' => current($value)]; - } + return (int) $v; + }) + ->end() + ->end() + ->booleanNode('use_locking')->defaultFalse()->end() // stream and rotating + ->scalarNode('filename_format')->defaultValue('{filename}-{date}')->end() //rotating + ->scalarNode('date_format')->defaultValue('Y-m-d')->end() //rotating + ->scalarNode('ident')->defaultFalse()->end() // syslog and syslogudp + ->scalarNode('logopts')->defaultValue(LOG_PID)->end() // syslog + ->scalarNode('facility')->defaultValue('user')->end() // syslog + ->scalarNode('max_files')->defaultValue(0)->end() // rotating + ->scalarNode('action_level')->defaultValue('WARNING')->end() // fingers_crossed + ->scalarNode('activation_strategy')->defaultNull()->end() // fingers_crossed + ->booleanNode('stop_buffering')->defaultTrue()->end()// fingers_crossed + ->scalarNode('passthru_level')->defaultNull()->end() // fingers_crossed + ->arrayNode('excluded_404s') // fingers_crossed + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->arrayNode('excluded_http_codes') // fingers_crossed + ->canBeUnset() + ->beforeNormalization() + ->always(function ($values) { + return array_map(function ($value) { + /* + * Allows YAML: + * excluded_http_codes: [403, 404, { 400: ['^/foo', '^/bar'] }] + * + * and XML: + * + * ^/foo + * ^/bar + * + * + */ - return ['code' => $value, 'urls' => []]; - }, $values); - }) - ->end() - ->prototype('array') - ->children() - ->scalarNode('code')->end() - ->arrayNode('urls') - ->prototype('scalar')->end() - ->end() - ->end() - ->end() - ->end() - ->arrayNode('accepted_levels') // filter - ->canBeUnset() - ->prototype('scalar')->end() - ->end() - ->scalarNode('min_level')->defaultValue('DEBUG')->end() // filter - ->scalarNode('max_level')->defaultValue('EMERGENCY')->end() //filter - ->scalarNode('buffer_size')->defaultValue(0)->end() // fingers_crossed and buffer - ->booleanNode('flush_on_overflow')->defaultFalse()->end() // buffer - ->scalarNode('handler')->end() // fingers_crossed and buffer - ->scalarNode('url')->end() // cube - ->scalarNode('exchange')->end() // amqp - ->scalarNode('exchange_name')->defaultValue('log')->end() // amqp - ->scalarNode('room')->end() // hipchat - ->scalarNode('message_format')->defaultValue('text')->end() // hipchat - ->scalarNode('api_version')->defaultNull()->end() // hipchat - ->scalarNode('channel')->defaultNull()->end() // slack & slackwebhook & slackbot - ->scalarNode('bot_name')->defaultValue('Monolog')->end() // slack & slackwebhook - ->scalarNode('use_attachment')->defaultTrue()->end() // slack & slackwebhook - ->scalarNode('use_short_attachment')->defaultFalse()->end() // slack & slackwebhook - ->scalarNode('include_extra')->defaultFalse()->end() // slack & slackwebhook - ->scalarNode('icon_emoji')->defaultNull()->end() // slack & slackwebhook - ->scalarNode('webhook_url')->end() // slackwebhook - ->scalarNode('team')->end() // slackbot - ->scalarNode('notify')->defaultFalse()->end() // hipchat - ->scalarNode('nickname')->defaultValue('Monolog')->end() // hipchat - ->scalarNode('token')->end() // pushover & hipchat & loggly & logentries & flowdock & rollbar & slack & slackbot & insightops - ->scalarNode('region')->end() // insightops - ->scalarNode('source')->end() // flowdock - ->booleanNode('use_ssl')->defaultTrue()->end() // logentries & hipchat & insightops - ->variableNode('user') // pushover - ->validate() - ->ifTrue(function ($v) { - return !is_string($v) && !is_array($v); - }) - ->thenInvalid('User must be a string or an array.') - ->end() - ->end() - ->scalarNode('title')->defaultNull()->end() // pushover - ->scalarNode('host')->defaultNull()->end() // syslogudp & hipchat - ->scalarNode('port')->defaultValue(514)->end() // syslogudp - ->arrayNode('publisher') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->end() - ->scalarNode('hostname')->end() - ->scalarNode('port')->defaultValue(12201)->end() - ->scalarNode('chunk_size')->defaultValue(1420)->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return !isset($v['id']) && !isset($v['hostname']); - }) - ->thenInvalid('What must be set is either the hostname or the id.') - ->end() - ->end() // gelf - ->arrayNode('mongo') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->end() - ->scalarNode('host')->end() - ->scalarNode('port')->defaultValue(27017)->end() - ->scalarNode('user')->end() - ->scalarNode('pass')->end() - ->scalarNode('database')->defaultValue('monolog')->end() - ->scalarNode('collection')->defaultValue('logs')->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return !isset($v['id']) && !isset($v['host']); - }) - ->thenInvalid('What must be set is either the host or the id.') - ->end() - ->validate() - ->ifTrue(function ($v) { - return isset($v['user']) && !isset($v['pass']); - }) - ->thenInvalid('If you set user, you must provide a password.') - ->end() - ->end() // mongo - ->arrayNode('elasticsearch') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->end() - ->scalarNode('host')->end() - ->scalarNode('port')->defaultValue(9200)->end() - ->scalarNode('transport')->defaultValue('Http')->end() - ->scalarNode('user')->defaultNull()->end() - ->scalarNode('password')->defaultNull()->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return !isset($v['id']) && !isset($v['host']); - }) - ->thenInvalid('What must be set is either the host or the id.') - ->end() - ->end() // elasticsearch - ->scalarNode('index')->defaultValue('monolog')->end() // elasticsearch - ->scalarNode('document_type')->defaultValue('logs')->end() // elasticsearch - ->scalarNode('ignore_error')->defaultValue(false)->end() // elasticsearch - ->arrayNode('redis') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->end() - ->scalarNode('host')->end() - ->scalarNode('password')->defaultNull()->end() - ->scalarNode('port')->defaultValue(6379)->end() - ->scalarNode('database')->defaultValue(0)->end() - ->scalarNode('key_name')->defaultValue('monolog_redis')->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return !isset($v['id']) && !isset($v['host']); - }) - ->thenInvalid('What must be set is either the host or the service id of the Redis client.') - ->end() - ->end() // redis - ->arrayNode('predis') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->end() - ->scalarNode('host')->end() - ->end() - ->validate() - ->ifTrue(function ($v) { - return !isset($v['id']) && !isset($v['host']); - }) - ->thenInvalid('What must be set is either the host or the service id of the Predis client.') - ->end() - ->end() // predis - ->arrayNode('config') - ->canBeUnset() - ->prototype('scalar')->end() - ->end() // rollbar - ->arrayNode('members') // group, whatfailuregroup - ->canBeUnset() - ->performNoDeepMerging() - ->prototype('scalar')->end() - ->end() - ->scalarNode('from_email')->end() // swift_mailer, native_mailer, symfony_mailer and flowdock - ->arrayNode('to_email') // swift_mailer, native_mailer and symfony_mailer - ->prototype('scalar')->end() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return [$v]; }) - ->end() - ->end() - ->scalarNode('subject')->end() // swift_mailer, native_mailer and symfony_mailer - ->scalarNode('content_type')->defaultNull()->end() // swift_mailer and symfony_mailer - ->arrayNode('headers') // native_mailer - ->canBeUnset() - ->scalarPrototype()->end() - ->end() - ->scalarNode('mailer')->defaultNull()->end() // swift_mailer and symfony_mailer - ->arrayNode('email_prototype') // swift_mailer and symfony_mailer - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['id' => $v]; }) - ->end() - ->children() - ->scalarNode('id')->isRequired()->end() - ->scalarNode('method')->defaultNull()->end() - ->end() - ->end() - ->booleanNode('lazy')->defaultValue(true)->end() // swift_mailer - ->scalarNode('connection_string')->end() // socket_handler - ->scalarNode('timeout')->end() // socket_handler, logentries, pushover, hipchat & slack - ->scalarNode('time')->defaultValue(60)->end() // deduplication - ->scalarNode('deduplication_level')->defaultValue(Logger::ERROR)->end() // deduplication - ->scalarNode('store')->defaultNull()->end() // deduplication - ->scalarNode('connection_timeout')->end() // socket_handler, logentries, pushover, hipchat & slack - ->booleanNode('persistent')->end() // socket_handler - ->scalarNode('dsn')->end() // raven_handler, sentry_handler - ->scalarNode('hub_id')->defaultNull()->end() // sentry_handler - ->scalarNode('client_id')->defaultNull()->end() // raven_handler, sentry_handler - ->scalarNode('auto_log_stacks')->defaultFalse()->end() // raven_handler - ->scalarNode('release')->defaultNull()->end() // raven_handler, sentry_handler - ->scalarNode('environment')->defaultNull()->end() // raven_handler, sentry_handler - ->scalarNode('message_type')->defaultValue(0)->end() // error_log - ->arrayNode('tags') // loggly - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return explode(',', $v); }) - ->end() - ->beforeNormalization() - ->ifArray() - ->then(function ($v) { return array_filter(array_map('trim', $v)); }) - ->end() + if (is_array($value)) { + return isset($value['code']) ? $value : ['code' => key($value), 'urls' => current($value)]; + } + + return ['code' => $value, 'urls' => []]; + }, $values); + }) + ->end() + ->prototype('array') + ->children() + ->scalarNode('code')->end() + ->arrayNode('urls') ->prototype('scalar')->end() ->end() - // console - ->variableNode('console_formater_options') - ->setDeprecated(...$this->getDeprecationMsg('"%path%.%node%" is deprecated, use "%path%.console_formatter_options" instead.', 3.7)) - ->validate() - ->ifTrue(function ($v) { - return !is_array($v); - }) - ->thenInvalid('The console_formater_options must be an array.') - ->end() - ->end() - ->variableNode('console_formatter_options') - ->defaultValue([]) - ->validate() - ->ifTrue(static function ($v) { return !is_array($v); }) - ->thenInvalid('The console_formatter_options must be an array.') - ->end() - ->end() - ->arrayNode('verbosity_levels') // console - ->beforeNormalization() - ->ifArray() - ->then(function ($v) { - $map = []; - $verbosities = ['VERBOSITY_QUIET', 'VERBOSITY_NORMAL', 'VERBOSITY_VERBOSE', 'VERBOSITY_VERY_VERBOSE', 'VERBOSITY_DEBUG']; - // allow numeric indexed array with ascendning verbosity and lowercase names of the constants - foreach ($v as $verbosity => $level) { - if (is_int($verbosity) && isset($verbosities[$verbosity])) { - $map[$verbosities[$verbosity]] = strtoupper($level); - } else { - $map[strtoupper($verbosity)] = strtoupper($level); - } - } + ->end() + ->end() + ->end() + ->arrayNode('accepted_levels') // filter + ->canBeUnset() + ->prototype('scalar')->end() + ->end() + ->scalarNode('min_level')->defaultValue('DEBUG')->end() // filter + ->scalarNode('max_level')->defaultValue('EMERGENCY')->end() //filter + ->scalarNode('buffer_size')->defaultValue(0)->end() // fingers_crossed and buffer + ->booleanNode('flush_on_overflow')->defaultFalse()->end() // buffer + ->scalarNode('handler')->end() // fingers_crossed and buffer + ->scalarNode('url')->end() // cube + ->scalarNode('exchange')->end() // amqp + ->scalarNode('exchange_name')->defaultValue('log')->end() // amqp + ->scalarNode('room')->end() // hipchat + ->scalarNode('message_format')->defaultValue('text')->end() // hipchat + ->scalarNode('api_version')->defaultNull()->end() // hipchat + ->scalarNode('channel')->defaultNull()->end() // slack & slackwebhook & slackbot + ->scalarNode('bot_name')->defaultValue('Monolog')->end() // slack & slackwebhook + ->scalarNode('use_attachment')->defaultTrue()->end() // slack & slackwebhook + ->scalarNode('use_short_attachment')->defaultFalse()->end() // slack & slackwebhook + ->scalarNode('include_extra')->defaultFalse()->end() // slack & slackwebhook + ->scalarNode('icon_emoji')->defaultNull()->end() // slack & slackwebhook + ->scalarNode('webhook_url')->end() // slackwebhook + ->scalarNode('team')->end() // slackbot + ->scalarNode('notify')->defaultFalse()->end() // hipchat + ->scalarNode('nickname')->defaultValue('Monolog')->end() // hipchat + ->scalarNode('token')->end() // pushover & hipchat & loggly & logentries & flowdock & rollbar & slack & slackbot & insightops + ->scalarNode('region')->end() // insightops + ->scalarNode('source')->end() // flowdock + ->booleanNode('use_ssl')->defaultTrue()->end() // logentries & hipchat & insightops + ->variableNode('user') // pushover + ->validate() + ->ifTrue(function ($v) { + return !is_string($v) && !is_array($v); + }) + ->thenInvalid('User must be a string or an array.') + ->end() + ->end() + ->scalarNode('title')->defaultNull()->end() // pushover + ->scalarNode('host')->defaultNull()->end() // syslogudp & hipchat + ->scalarNode('port')->defaultValue(514)->end() // syslogudp + ->arrayNode('config') + ->canBeUnset() + ->prototype('scalar')->end() + ->end() // rollbar + ->arrayNode('members') // group, whatfailuregroup + ->canBeUnset() + ->performNoDeepMerging() + ->prototype('scalar')->end() + ->end() + ->scalarNode('connection_string')->end() // socket_handler + ->scalarNode('timeout')->end() // socket_handler, logentries, pushover, hipchat & slack + ->scalarNode('time')->defaultValue(60)->end() // deduplication + ->scalarNode('deduplication_level')->defaultValue(Logger::ERROR)->end() // deduplication + ->scalarNode('store')->defaultNull()->end() // deduplication + ->scalarNode('connection_timeout')->end() // socket_handler, logentries, pushover, hipchat & slack + ->booleanNode('persistent')->end() // socket_handler + ->scalarNode('dsn')->end() // raven_handler, sentry_handler + ->scalarNode('hub_id')->defaultNull()->end() // sentry_handler + ->scalarNode('client_id')->defaultNull()->end() // raven_handler, sentry_handler + ->scalarNode('auto_log_stacks')->defaultFalse()->end() // raven_handler + ->scalarNode('release')->defaultNull()->end() // raven_handler, sentry_handler + ->scalarNode('environment')->defaultNull()->end() // raven_handler, sentry_handler + ->scalarNode('message_type')->defaultValue(0)->end() // error_log + ->arrayNode('tags') // loggly + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return explode(',', $v); }) + ->end() + ->beforeNormalization() + ->ifArray() + ->then(function ($v) { return array_filter(array_map('trim', $v)); }) + ->end() + ->prototype('scalar')->end() + ->end() + // console + ->variableNode('console_formater_options') + ->setDeprecated(...$this->getDeprecationMsg('"%path%.%node%" is deprecated, use "%path%.console_formatter_options" instead.', 3.7)) + ->validate() + ->ifTrue(function ($v) { + return !is_array($v); + }) + ->thenInvalid('The console_formater_options must be an array.') + ->end() + ->end() + ->variableNode('console_formatter_options') + ->defaultValue([]) + ->validate() + ->ifTrue(static function ($v) { return !is_array($v); }) + ->thenInvalid('The console_formatter_options must be an array.') + ->end() + ->end() + ->scalarNode('formatter')->end() + ->booleanNode('nested')->defaultFalse()->end() + ->end(); - return $map; - }) - ->end() - ->children() - ->scalarNode('VERBOSITY_QUIET')->defaultValue('ERROR')->end() - ->scalarNode('VERBOSITY_NORMAL')->defaultValue('WARNING')->end() - ->scalarNode('VERBOSITY_VERBOSE')->defaultValue('NOTICE')->end() - ->scalarNode('VERBOSITY_VERY_VERBOSE')->defaultValue('INFO')->end() - ->scalarNode('VERBOSITY_DEBUG')->defaultValue('DEBUG')->end() - ->end() - ->validate() - ->always(function ($v) { - $map = []; - foreach ($v as $verbosity => $level) { - $verbosityConstant = 'Symfony\Component\Console\Output\OutputInterface::'.$verbosity; + $this->addGelfSection($handlerNode); + $this->addMongoSection($handlerNode); + $this->addElasticsearchSection($handlerNode); + $this->addRedisSection($handlerNode); + $this->addPredisSection($handlerNode); + $this->addMailerSection($handlerNode); + $this->addVerbosityLevelSection($handlerNode); + $this->addChannelsSection($handlerNode); - if (!defined($verbosityConstant)) { - throw new InvalidConfigurationException(sprintf( - 'The configured verbosity "%s" is invalid as it is not defined in Symfony\Component\Console\Output\OutputInterface.', - $verbosity - )); - } - if (!is_numeric($level)) { - $levelConstant = 'Monolog\Logger::'.$level; - if (!defined($levelConstant)) { - throw new InvalidConfigurationException(sprintf( - 'The configured minimum log level "%s" for verbosity "%s" is invalid as it is not defined in Monolog\Logger.', - $level, $verbosity - )); - } - $level = constant($levelConstant); - } else { - $level = (int) $level; - } + $handlerNode + ->beforeNormalization() + ->always(static function ($v) { + if (empty($v['console_formatter_options']) && !empty($v['console_formater_options'])) { + $v['console_formatter_options'] = $v['console_formater_options']; + } - $map[constant($verbosityConstant)] = $level; - } + return $v; + }) + ->end() + ->validate() + ->always(static function ($v) { unset($v['console_formater_options']); return $v; }) + ->end() + ->validate() + ->ifTrue(function ($v) { return 'service' === $v['type'] && !empty($v['formatter']); }) + ->thenInvalid('Service handlers can not have a formatter configured in the bundle, you must reconfigure the service itself instead') + ->end() + ->validate() + ->ifTrue(function ($v) { return ('fingers_crossed' === $v['type'] || 'buffer' === $v['type'] || 'filter' === $v['type']) && empty($v['handler']); }) + ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler or BufferHandler or FilterHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_404s']) && !empty($v['activation_strategy']); }) + ->thenInvalid('You can not use excluded_404s together with a custom activation_strategy in a FingersCrossedHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['activation_strategy']); }) + ->thenInvalid('You can not use excluded_http_codes together with a custom activation_strategy in a FingersCrossedHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['excluded_404s']); }) + ->thenInvalid('You can not use excluded_http_codes together with excluded_404s in a FingersCrossedHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'fingers_crossed' !== $v['type'] && (!empty($v['excluded_http_codes']) || !empty($v['excluded_404s'])); }) + ->thenInvalid('You can only use excluded_http_codes/excluded_404s with a FingersCrossedHandler definition') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'filter' === $v['type'] && "DEBUG" !== $v['min_level'] && !empty($v['accepted_levels']); }) + ->thenInvalid('You can not use min_level together with accepted_levels in a FilterHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'filter' === $v['type'] && "EMERGENCY" !== $v['max_level'] && !empty($v['accepted_levels']); }) + ->thenInvalid('You can not use max_level together with accepted_levels in a FilterHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && !empty($v['id']) && !empty($v['token']); }) + ->thenInvalid('You can not use both an id and a token in a RollbarHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && empty($v['id']) && empty($v['token']); }) + ->thenInvalid('The id or the token has to be specified to use a RollbarHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'service' === $v['type'] && !isset($v['id']); }) + ->thenInvalid('The id has to be specified to use a service as handler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'syslogudp' === $v['type'] && !isset($v['host']); }) + ->thenInvalid('The host has to be specified to use a syslogudp as handler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'socket' === $v['type'] && !isset($v['connection_string']); }) + ->thenInvalid('The connection_string has to be specified to use a SocketHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'pushover' === $v['type'] && (empty($v['token']) || empty($v['user'])); }) + ->thenInvalid('The token and user have to be specified to use a PushoverHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'raven' === $v['type'] && !array_key_exists('dsn', $v) && null === $v['client_id']; }) + ->thenInvalid('The DSN has to be specified to use a RavenHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'sentry' === $v['type'] && !array_key_exists('dsn', $v) && null === $v['hub_id'] && null === $v['client_id']; }) + ->thenInvalid('The DSN has to be specified to use Sentry\'s handler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'sentry' === $v['type'] && null !== $v['hub_id'] && null !== $v['client_id']; }) + ->thenInvalid('You can not use both a hub_id and a client_id in a Sentry handler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && (empty($v['token']) || empty($v['room'])); }) + ->thenInvalid('The token and room have to be specified to use a HipChatHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && !in_array($v['message_format'], ['text', 'html']); }) + ->thenInvalid('The message_format has to be "text" or "html" in a HipChatHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && null !== $v['api_version'] && !in_array($v['api_version'], ['v1', 'v2'], true); }) + ->thenInvalid('The api_version has to be "v1" or "v2" in a HipChatHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'slack' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) + ->thenInvalid('The token and channel have to be specified to use a SlackHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'slackwebhook' === $v['type'] && (empty($v['webhook_url'])); }) + ->thenInvalid('The webhook_url have to be specified to use a SlackWebhookHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'slackbot' === $v['type'] && (empty($v['team']) || empty($v['token']) || empty($v['channel'])); }) + ->thenInvalid('The team, token and channel have to be specified to use a SlackbotHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'cube' === $v['type'] && empty($v['url']); }) + ->thenInvalid('The url has to be specified to use a CubeHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'amqp' === $v['type'] && empty($v['exchange']); }) + ->thenInvalid('The exchange has to be specified to use a AmqpHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'loggly' === $v['type'] && empty($v['token']); }) + ->thenInvalid('The token has to be specified to use a LogglyHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'loggly' === $v['type'] && !empty($v['tags']); }) + ->then(function ($v) { + $invalidTags = preg_grep('/^[a-z0-9][a-z0-9\.\-_]*$/i', $v['tags'], PREG_GREP_INVERT); + if (!empty($invalidTags)) { + throw new InvalidConfigurationException(sprintf('The following Loggly tags are invalid: %s.', implode(', ', $invalidTags))); + } - return $map; - }) - ->end() - ->end() - ->arrayNode('channels') - ->fixXmlConfig('channel', 'elements') - ->canBeUnset() - ->beforeNormalization() - ->ifString() - ->then(function ($v) { return ['elements' => [$v]]; }) - ->end() - ->beforeNormalization() - ->ifTrue(function ($v) { return is_array($v) && is_numeric(key($v)); }) - ->then(function ($v) { return ['elements' => $v]; }) - ->end() - ->validate() - ->ifTrue(function ($v) { return empty($v); }) - ->thenUnset() - ->end() - ->validate() - ->always(function ($v) { - $isExclusive = null; - if (isset($v['type'])) { - $isExclusive = 'exclusive' === $v['type']; - } + return $v; + }) + ->end() + ->validate() + ->ifTrue(function ($v) { return 'logentries' === $v['type'] && empty($v['token']); }) + ->thenInvalid('The token has to be specified to use a LogEntriesHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'insightops' === $v['type'] && empty($v['token']); }) + ->thenInvalid('The token has to be specified to use a InsightOpsHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['token']); }) + ->thenInvalid('The token has to be specified to use a FlowdockHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['from_email']); }) + ->thenInvalid('The from_email has to be specified to use a FlowdockHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['source']); }) + ->thenInvalid('The source has to be specified to use a FlowdockHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'server_log' === $v['type'] && empty($v['host']); }) + ->thenInvalid('The host has to be specified to use a ServerLogHandler') + ->end(); - $elements = []; - foreach ($v['elements'] as $element) { - if (0 === strpos($element, '!')) { - if (false === $isExclusive) { - throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.'); - } - $elements[] = substr($element, 1); - $isExclusive = true; - } else { - if (true === $isExclusive) { - throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list'); - } - $elements[] = $element; - $isExclusive = false; - } - } + $rootNode + ->validate() + ->ifTrue(function ($v) { return isset($v['debug']); }) + ->thenInvalid('The "debug" name cannot be used as it is reserved for the handler of the profiler') + ->end() + ->example([ + 'syslog' => [ + 'type' => 'stream', + 'path' => '/var/log/symfony.log', + 'level' => 'ERROR', + 'bubble' => 'false', + 'formatter' => 'my_formatter', + ], + 'main' => [ + 'type' => 'fingers_crossed', + 'action_level' => 'WARNING', + 'buffer_size' => 30, + 'handler' => 'custom', + ], + 'custom' => [ + 'type' => 'service', + 'id' => 'my_handler', + ] + ]) + ; - if (!count($elements)) { - return null; - } + return $treeBuilder; + } - return ['type' => $isExclusive ? 'exclusive' : 'inclusive', 'elements' => $elements]; - }) - ->end() - ->children() - ->scalarNode('type') - ->validate() - ->ifNotInArray(['inclusive', 'exclusive']) - ->thenInvalid('The type of channels has to be inclusive or exclusive') - ->end() - ->end() - ->arrayNode('elements') - ->prototype('scalar')->end() - ->end() - ->end() - ->end() - ->scalarNode('formatter')->end() - ->booleanNode('nested')->defaultFalse()->end() - ->end() - ->beforeNormalization() - ->always(static function ($v) { - if (empty($v['console_formatter_options']) && !empty($v['console_formater_options'])) { - $v['console_formatter_options'] = $v['console_formater_options']; - } + private function addGelfSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('publisher') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('hostname')->end() + ->scalarNode('port')->defaultValue(12201)->end() + ->scalarNode('chunk_size')->defaultValue(1420)->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['hostname']); + }) + ->thenInvalid('What must be set is either the hostname or the id.') + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { return 'gelf' === $v['type'] && !isset($v['publisher']); }) + ->thenInvalid('The publisher has to be specified to use a GelfHandler') + ->end() + ; + } - return $v; - }) - ->end() - ->validate() - ->always(static function ($v) { unset($v['console_formater_options']); return $v; }) - ->end() - ->validate() - ->ifTrue(function ($v) { return 'service' === $v['type'] && !empty($v['formatter']); }) - ->thenInvalid('Service handlers can not have a formatter configured in the bundle, you must reconfigure the service itself instead') - ->end() - ->validate() - ->ifTrue(function ($v) { return ('fingers_crossed' === $v['type'] || 'buffer' === $v['type'] || 'filter' === $v['type']) && empty($v['handler']); }) - ->thenInvalid('The handler has to be specified to use a FingersCrossedHandler or BufferHandler or FilterHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_404s']) && !empty($v['activation_strategy']); }) - ->thenInvalid('You can not use excluded_404s together with a custom activation_strategy in a FingersCrossedHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['activation_strategy']); }) - ->thenInvalid('You can not use excluded_http_codes together with a custom activation_strategy in a FingersCrossedHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'fingers_crossed' === $v['type'] && !empty($v['excluded_http_codes']) && !empty($v['excluded_404s']); }) - ->thenInvalid('You can not use excluded_http_codes together with excluded_404s in a FingersCrossedHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'fingers_crossed' !== $v['type'] && (!empty($v['excluded_http_codes']) || !empty($v['excluded_404s'])); }) - ->thenInvalid('You can only use excluded_http_codes/excluded_404s with a FingersCrossedHandler definition') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'filter' === $v['type'] && "DEBUG" !== $v['min_level'] && !empty($v['accepted_levels']); }) - ->thenInvalid('You can not use min_level together with accepted_levels in a FilterHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'filter' === $v['type'] && "EMERGENCY" !== $v['max_level'] && !empty($v['accepted_levels']); }) - ->thenInvalid('You can not use max_level together with accepted_levels in a FilterHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && !empty($v['id']) && !empty($v['token']); }) - ->thenInvalid('You can not use both an id and a token in a RollbarHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'rollbar' === $v['type'] && empty($v['id']) && empty($v['token']); }) - ->thenInvalid('The id or the token has to be specified to use a RollbarHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'swift_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) - ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use a SwiftMailerHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'native_mailer' === $v['type'] && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) - ->thenInvalid('The sender, recipient and subject have to be specified to use a NativeMailerHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'symfony_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) - ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use the Symfony MailerHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'service' === $v['type'] && !isset($v['id']); }) - ->thenInvalid('The id has to be specified to use a service as handler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'syslogudp' === $v['type'] && !isset($v['host']); }) - ->thenInvalid('The host has to be specified to use a syslogudp as handler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'gelf' === $v['type'] && !isset($v['publisher']); }) - ->thenInvalid('The publisher has to be specified to use a GelfHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'socket' === $v['type'] && !isset($v['connection_string']); }) - ->thenInvalid('The connection_string has to be specified to use a SocketHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'pushover' === $v['type'] && (empty($v['token']) || empty($v['user'])); }) - ->thenInvalid('The token and user have to be specified to use a PushoverHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'raven' === $v['type'] && !array_key_exists('dsn', $v) && null === $v['client_id']; }) - ->thenInvalid('The DSN has to be specified to use a RavenHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'sentry' === $v['type'] && !array_key_exists('dsn', $v) && null === $v['hub_id'] && null === $v['client_id']; }) - ->thenInvalid('The DSN has to be specified to use Sentry\'s handler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'sentry' === $v['type'] && null !== $v['hub_id'] && null !== $v['client_id']; }) - ->thenInvalid('You can not use both a hub_id and a client_id in a Sentry handler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && (empty($v['token']) || empty($v['room'])); }) - ->thenInvalid('The token and room have to be specified to use a HipChatHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && !in_array($v['message_format'], ['text', 'html']); }) - ->thenInvalid('The message_format has to be "text" or "html" in a HipChatHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'hipchat' === $v['type'] && null !== $v['api_version'] && !in_array($v['api_version'], ['v1', 'v2'], true); }) - ->thenInvalid('The api_version has to be "v1" or "v2" in a HipChatHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'slack' === $v['type'] && (empty($v['token']) || empty($v['channel'])); }) - ->thenInvalid('The token and channel have to be specified to use a SlackHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'slackwebhook' === $v['type'] && (empty($v['webhook_url'])); }) - ->thenInvalid('The webhook_url have to be specified to use a SlackWebhookHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'slackbot' === $v['type'] && (empty($v['team']) || empty($v['token']) || empty($v['channel'])); }) - ->thenInvalid('The team, token and channel have to be specified to use a SlackbotHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'cube' === $v['type'] && empty($v['url']); }) - ->thenInvalid('The url has to be specified to use a CubeHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'mongo' === $v['type'] && !isset($v['mongo']); }) - ->thenInvalid('The mongo configuration has to be specified to use a MongoHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'amqp' === $v['type'] && empty($v['exchange']); }) - ->thenInvalid('The exchange has to be specified to use a AmqpHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'loggly' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a LogglyHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'loggly' === $v['type'] && !empty($v['tags']); }) - ->then(function ($v) { - $invalidTags = preg_grep('/^[a-z0-9][a-z0-9\.\-_]*$/i', $v['tags'], PREG_GREP_INVERT); - if (!empty($invalidTags)) { - throw new InvalidConfigurationException(sprintf('The following Loggly tags are invalid: %s.', implode(', ', $invalidTags))); + private function addMongoSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('mongo') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('host')->end() + ->scalarNode('port')->defaultValue(27017)->end() + ->scalarNode('user')->end() + ->scalarNode('pass')->end() + ->scalarNode('database')->defaultValue('monolog')->end() + ->scalarNode('collection')->defaultValue('logs')->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['host']); + }) + ->thenInvalid('What must be set is either the host or the id.') + ->end() + ->validate() + ->ifTrue(function ($v) { + return isset($v['user']) && !isset($v['pass']); + }) + ->thenInvalid('If you set user, you must provide a password.') + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { return 'mongo' === $v['type'] && !isset($v['mongo']); }) + ->thenInvalid('The mongo configuration has to be specified to use a MongoHandler') + ->end() + ; + } + + private function addElasticsearchSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('elasticsearch') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('host')->end() + ->scalarNode('port')->defaultValue(9200)->end() + ->scalarNode('transport')->defaultValue('Http')->end() + ->scalarNode('user')->defaultNull()->end() + ->scalarNode('password')->defaultNull()->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['host']); + }) + ->thenInvalid('What must be set is either the host or the id.') + ->end() + ->end() + ->scalarNode('index')->defaultValue('monolog')->end() // elasticsearch + ->scalarNode('document_type')->defaultValue('logs')->end() // elasticsearch + ->scalarNode('ignore_error')->defaultValue(false)->end() // elasticsearch + ->end() + ; + } + + private function addRedisSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('redis') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('host')->end() + ->scalarNode('password')->defaultNull()->end() + ->scalarNode('port')->defaultValue(6379)->end() + ->scalarNode('database')->defaultValue(0)->end() + ->scalarNode('key_name')->defaultValue('monolog_redis')->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['host']); + }) + ->thenInvalid('What must be set is either the host or the service id of the Redis client.') + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { return 'redis' === $v['type'] && empty($v['redis']); }) + ->thenInvalid('The host has to be specified to use a RedisLogHandler') + ->end() + ; + } + + private function addPredisSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('predis') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->end() + ->scalarNode('host')->end() + ->end() + ->validate() + ->ifTrue(function ($v) { + return !isset($v['id']) && !isset($v['host']); + }) + ->thenInvalid('What must be set is either the host or the service id of the Predis client.') + ->end() + ->end() + ->end() + ->validate() + ->ifTrue(function ($v) { return 'predis' === $v['type'] && empty($v['redis']); }) + ->thenInvalid('The host has to be specified to use a RedisLogHandler') + ->end() + ; + } + + private function addMailerSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->scalarNode('from_email')->end() // swift_mailer, native_mailer, symfony_mailer and flowdock + ->arrayNode('to_email') // swift_mailer, native_mailer and symfony_mailer + ->prototype('scalar')->end() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return [$v]; }) + ->end() + ->end() + ->scalarNode('subject')->end() // swift_mailer, native_mailer and symfony_mailer + ->scalarNode('content_type')->defaultNull()->end() // swift_mailer and symfony_mailer + ->arrayNode('headers') // native_mailer + ->canBeUnset() + ->scalarPrototype()->end() + ->end() + ->scalarNode('mailer')->defaultNull()->end() // swift_mailer and symfony_mailer + ->arrayNode('email_prototype') // swift_mailer and symfony_mailer + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['id' => $v]; }) + ->end() + ->children() + ->scalarNode('id')->isRequired()->end() + ->scalarNode('method')->defaultNull()->end() + ->end() + ->end() + ->booleanNode('lazy')->defaultValue(true)->end() // swift_mailer + ->end() + ->validate() + ->ifTrue(function ($v) { return 'swift_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) + ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use a SwiftMailerHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'native_mailer' === $v['type'] && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) + ->thenInvalid('The sender, recipient and subject have to be specified to use a NativeMailerHandler') + ->end() + ->validate() + ->ifTrue(function ($v) { return 'symfony_mailer' === $v['type'] && empty($v['email_prototype']) && (empty($v['from_email']) || empty($v['to_email']) || empty($v['subject'])); }) + ->thenInvalid('The sender, recipient and subject or an email prototype have to be specified to use the Symfony MailerHandler') + ->end() + ; + } + + private function addVerbosityLevelSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('verbosity_levels') // console + ->beforeNormalization() + ->ifArray() + ->then(function ($v) { + $map = []; + $verbosities = ['VERBOSITY_QUIET', 'VERBOSITY_NORMAL', 'VERBOSITY_VERBOSE', 'VERBOSITY_VERY_VERBOSE', 'VERBOSITY_DEBUG']; + // allow numeric indexed array with ascendning verbosity and lowercase names of the constants + foreach ($v as $verbosity => $level) { + if (is_int($verbosity) && isset($verbosities[$verbosity])) { + $map[$verbosities[$verbosity]] = strtoupper($level); + } else { + $map[strtoupper($verbosity)] = strtoupper($level); } + } - return $v; - }) - ->end() - ->validate() - ->ifTrue(function ($v) { return 'logentries' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a LogEntriesHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'insightops' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a InsightOpsHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['token']); }) - ->thenInvalid('The token has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['from_email']); }) - ->thenInvalid('The from_email has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'flowdock' === $v['type'] && empty($v['source']); }) - ->thenInvalid('The source has to be specified to use a FlowdockHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'server_log' === $v['type'] && empty($v['host']); }) - ->thenInvalid('The host has to be specified to use a ServerLogHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'redis' === $v['type'] && empty($v['redis']); }) - ->thenInvalid('The host has to be specified to use a RedisLogHandler') - ->end() - ->validate() - ->ifTrue(function ($v) { return 'predis' === $v['type'] && empty($v['redis']); }) - ->thenInvalid('The host has to be specified to use a RedisLogHandler') - ->end() + return $map; + }) + ->end() + ->children() + ->scalarNode('VERBOSITY_QUIET')->defaultValue('ERROR')->end() + ->scalarNode('VERBOSITY_NORMAL')->defaultValue('WARNING')->end() + ->scalarNode('VERBOSITY_VERBOSE')->defaultValue('NOTICE')->end() + ->scalarNode('VERBOSITY_VERY_VERBOSE')->defaultValue('INFO')->end() + ->scalarNode('VERBOSITY_DEBUG')->defaultValue('DEBUG')->end() ->end() ->validate() - ->ifTrue(function ($v) { return isset($v['debug']); }) - ->thenInvalid('The "debug" name cannot be used as it is reserved for the handler of the profiler') + ->always(function ($v) { + $map = []; + foreach ($v as $verbosity => $level) { + $verbosityConstant = 'Symfony\Component\Console\Output\OutputInterface::'.$verbosity; + + if (!defined($verbosityConstant)) { + throw new InvalidConfigurationException(sprintf( + 'The configured verbosity "%s" is invalid as it is not defined in Symfony\Component\Console\Output\OutputInterface.', + $verbosity + )); + } + if (!is_numeric($level)) { + $levelConstant = 'Monolog\Logger::'.$level; + if (!defined($levelConstant)) { + throw new InvalidConfigurationException(sprintf( + 'The configured minimum log level "%s" for verbosity "%s" is invalid as it is not defined in Monolog\Logger.', + $level, $verbosity + )); + } + $level = constant($levelConstant); + } else { + $level = (int) $level; + } + + $map[constant($verbosityConstant)] = $level; + } + + return $map; + }) ->end() - ->example([ - 'syslog' => [ - 'type' => 'stream', - 'path' => '/var/log/symfony.log', - 'level' => 'ERROR', - 'bubble' => 'false', - 'formatter' => 'my_formatter', - ], - 'main' => [ - 'type' => 'fingers_crossed', - 'action_level' => 'WARNING', - 'buffer_size' => 30, - 'handler' => 'custom', - ], - 'custom' => [ - 'type' => 'service', - 'id' => 'my_handler', - ] - ]) ->end() ->end() ; + } - return $treeBuilder; + private function addChannelsSection(ArrayNodeDefinition $handerNode) + { + $handerNode + ->children() + ->arrayNode('channels') + ->fixXmlConfig('channel', 'elements') + ->canBeUnset() + ->beforeNormalization() + ->ifString() + ->then(function ($v) { return ['elements' => [$v]]; }) + ->end() + ->beforeNormalization() + ->ifTrue(function ($v) { return is_array($v) && is_numeric(key($v)); }) + ->then(function ($v) { return ['elements' => $v]; }) + ->end() + ->validate() + ->ifTrue(function ($v) { return empty($v); }) + ->thenUnset() + ->end() + ->validate() + ->always(function ($v) { + $isExclusive = null; + if (isset($v['type'])) { + $isExclusive = 'exclusive' === $v['type']; + } + + $elements = []; + foreach ($v['elements'] as $element) { + if (0 === strpos($element, '!')) { + if (false === $isExclusive) { + throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list.'); + } + $elements[] = substr($element, 1); + $isExclusive = true; + } else { + if (true === $isExclusive) { + throw new InvalidConfigurationException('Cannot combine exclusive/inclusive definitions in channels list'); + } + $elements[] = $element; + $isExclusive = false; + } + } + + if (!count($elements)) { + return null; + } + + return ['type' => $isExclusive ? 'exclusive' : 'inclusive', 'elements' => $elements]; + }) + ->end() + ->children() + ->scalarNode('type') + ->validate() + ->ifNotInArray(['inclusive', 'exclusive']) + ->thenInvalid('The type of channels has to be inclusive or exclusive') + ->end() + ->end() + ->arrayNode('elements') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ; } /** From ca65d9220e36f1f9efd972169cf30f1c088c8382 Mon Sep 17 00:00:00 2001 From: Thomas Lallement Date: Thu, 21 Apr 2022 14:05:12 +0200 Subject: [PATCH 2/2] Fix config split (loss of validation part) --- DependencyInjection/Configuration.php | 76 ++++++++++++++------------- 1 file changed, 39 insertions(+), 37 deletions(-) diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index e091ba16..c87013f2 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -356,7 +356,7 @@ public function getConfigTreeBuilder() $treeBuilder = new TreeBuilder('monolog'); $rootNode = method_exists(TreeBuilder::class, 'getRootNode') ? $treeBuilder->getRootNode() : $treeBuilder->root('monolog'); - $handlerNode = $rootNode + $handlers = $rootNode ->fixXmlConfig('channel') ->fixXmlConfig('handler') ->children() @@ -365,17 +365,44 @@ public function getConfigTreeBuilder() ->canBeUnset() ->prototype('scalar')->end() ->end() - ->arrayNode('handlers') - ->canBeUnset() - ->useAttributeAsKey('name') - ->prototype('array') - ->fixXmlConfig('member') - ->fixXmlConfig('excluded_404') - ->fixXmlConfig('excluded_http_code') - ->fixXmlConfig('tag') - ->fixXmlConfig('accepted_level') - ->fixXmlConfig('header') - ->canBeUnset(); + ->arrayNode('handlers'); + + $handlers + ->canBeUnset() + ->useAttributeAsKey('name') + ->validate() + ->ifTrue(function ($v) { return isset($v['debug']); }) + ->thenInvalid('The "debug" name cannot be used as it is reserved for the handler of the profiler') + ->end() + ->example([ + 'syslog' => [ + 'type' => 'stream', + 'path' => '/var/log/symfony.log', + 'level' => 'ERROR', + 'bubble' => 'false', + 'formatter' => 'my_formatter', + ], + 'main' => [ + 'type' => 'fingers_crossed', + 'action_level' => 'WARNING', + 'buffer_size' => 30, + 'handler' => 'custom', + ], + 'custom' => [ + 'type' => 'service', + 'id' => 'my_handler', + ] + ]); + + $handlerNode = $handlers + ->prototype('array') + ->fixXmlConfig('member') + ->fixXmlConfig('excluded_404') + ->fixXmlConfig('excluded_http_code') + ->fixXmlConfig('tag') + ->fixXmlConfig('accepted_level') + ->fixXmlConfig('header') + ->canBeUnset(); $handlerNode ->children() @@ -712,32 +739,7 @@ public function getConfigTreeBuilder() ->validate() ->ifTrue(function ($v) { return 'server_log' === $v['type'] && empty($v['host']); }) ->thenInvalid('The host has to be specified to use a ServerLogHandler') - ->end(); - - $rootNode - ->validate() - ->ifTrue(function ($v) { return isset($v['debug']); }) - ->thenInvalid('The "debug" name cannot be used as it is reserved for the handler of the profiler') ->end() - ->example([ - 'syslog' => [ - 'type' => 'stream', - 'path' => '/var/log/symfony.log', - 'level' => 'ERROR', - 'bubble' => 'false', - 'formatter' => 'my_formatter', - ], - 'main' => [ - 'type' => 'fingers_crossed', - 'action_level' => 'WARNING', - 'buffer_size' => 30, - 'handler' => 'custom', - ], - 'custom' => [ - 'type' => 'service', - 'id' => 'my_handler', - ] - ]) ; return $treeBuilder;