Completed
Push — master ( 94d8f9...44e556 )
by Łukasz
41:18 queued 18:32
created

Configuration::addRouterSection()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 26
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
nc 1
nop 1
dl 0
loc 26
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * File containing the Configuration class.
5
 *
6
 * @copyright Copyright (C) eZ Systems AS. All rights reserved.
7
 * @license For full copyright and license information view LICENSE file distributed with this source code.
8
 */
9
namespace eZ\Bundle\EzPublishCoreBundle\DependencyInjection;
10
11
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\ParserInterface;
12
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\SiteAccessAware\Configuration as SiteAccessConfiguration;
13
use eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\Suggestion\Collector\SuggestionCollectorInterface;
14
use Symfony\Component\Config\Definition\Builder\NodeBuilder;
15
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
16
use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition;
17
18
class Configuration extends SiteAccessConfiguration
19
{
20
    const CUSTOM_TAG_ATTRIBUTE_TYPES = ['number', 'string', 'boolean', 'choice'];
21
22
    /**
23
     * @var \eZ\Bundle\EzPublishCoreBundle\DependencyInjection\Configuration\ParserInterface
24
     */
25
    private $mainConfigParser;
26
27
    /**
28
     * @var Configuration\Suggestion\Collector\SuggestionCollectorInterface
29
     */
30
    private $suggestionCollector;
31
32
    /**
33
     * @var \eZ\Bundle\EzPublishCoreBundle\SiteAccess\SiteAccessConfigurationFilter[]
34
     */
35
    private $siteAccessConfigurationFilters;
36
37
    public function __construct(ParserInterface $mainConfigParser, SuggestionCollectorInterface $suggestionCollector)
38
    {
39
        $this->suggestionCollector = $suggestionCollector;
40
        $this->mainConfigParser = $mainConfigParser;
41
    }
42
43
    public function setSiteAccessConfigurationFilters(array $filters)
44
    {
45
        $this->siteAccessConfigurationFilters = $filters;
46
    }
47
48
    /**
49
     * Generates the configuration tree builder.
50
     *
51
     * @return \Symfony\Component\Config\Definition\Builder\TreeBuilder The tree builder
52
     */
53
    public function getConfigTreeBuilder()
54
    {
55
        $treeBuilder = new TreeBuilder();
56
        $rootNode = $treeBuilder->root('ezpublish');
57
58
        $this->addRepositoriesSection($rootNode);
59
        $this->addSiteaccessSection($rootNode);
60
        $this->addImageMagickSection($rootNode);
61
        $this->addHttpCacheSection($rootNode);
62
        $this->addPageSection($rootNode);
63
        $this->addRouterSection($rootNode);
64
        $this->addRichTextSection($rootNode);
65
        $this->addUrlAliasSection($rootNode);
66
67
        // Delegate SiteAccess config to configuration parsers
68
        $this->mainConfigParser->addSemanticConfig($this->generateScopeBaseNode($rootNode));
69
70
        return $treeBuilder;
71
    }
72
73
    public function addRepositoriesSection(ArrayNodeDefinition $rootNode)
74
    {
75
        $rootNode
76
            ->children()
77
                ->arrayNode('repositories')
78
                    ->info('Content repositories configuration')
79
                    ->example(
80
                        array(
81
                            'main' => array(
82
                                'storage' => array(
83
                                    'engine' => 'legacy',
84
                                    'connection' => 'my_doctrine_connection_name',
85
                                ),
86
                            ),
87
                        )
88
                    )
89
                    ->useAttributeAsKey('alias')
90
                    ->prototype('array')
91
                        ->beforeNormalization()
92
                            ->always(
93
                                // Handling deprecated structure by mapping it to new one
94
                                function ($v) {
95
                                    if (isset($v['storage'])) {
96
                                        return $v;
97
                                    }
98
99
                                    if (isset($v['engine'])) {
100
                                        $v['storage']['engine'] = $v['engine'];
101
                                        unset($v['engine']);
102
                                    }
103
104
                                    if (isset($v['connection'])) {
105
                                        $v['storage']['connection'] = $v['connection'];
106
                                        unset($v['connection']);
107
                                    }
108
109
                                    if (isset($v['config'])) {
110
                                        $v['storage']['config'] = $v['config'];
111
                                        unset($v['config']);
112
                                    }
113
114
                                    return $v;
115
                                }
116
                            )
117
                        ->end()
118
                        ->beforeNormalization()
119
                            ->always(
120
                                // Setting default values
121
                                function ($v) {
122
                                    if ($v === null) {
123
                                        $v = array();
124
                                    }
125
126
                                    if (!isset($v['storage'])) {
127
                                        $v['storage'] = array();
128
                                    }
129
130
                                    if (!isset($v['search'])) {
131
                                        $v['search'] = array();
132
                                    }
133
134
                                    if (!isset($v['fields_groups']['list'])) {
135
                                        $v['fields_groups']['list'] = [];
136
                                    }
137
138
                                    if (!isset($v['options'])) {
139
                                        $v['options'] = [];
140
                                    }
141
142
                                    return $v;
143
                                }
144
                            )
145
                        ->end()
146
                        ->children()
147
                            ->arrayNode('storage')
148
                                ->children()
149
                                    ->scalarNode('engine')
150
                                        ->defaultValue('%ezpublish.api.storage_engine.default%')
151
                                        ->info('The storage engine to use')
152
                                    ->end()
153
                                    ->scalarNode('connection')
154
                                        ->defaultNull()
155
                                        ->info('The connection name, if applicable (e.g. Doctrine connection name). If not set, the default connection will be used.')
156
                                    ->end()
157
                                    ->arrayNode('config')
158
                                        ->info('Arbitrary configuration options, supported by your storage engine')
159
                                        ->useAttributeAsKey('key')
160
                                        ->prototype('variable')->end()
161
                                    ->end()
162
                                ->end()
163
                            ->end()
164
                            ->arrayNode('search')
165
                                ->children()
166
                                    ->scalarNode('engine')
167
                                        ->defaultValue('%ezpublish.api.search_engine.default%')
168
                                        ->info('The search engine to use')
169
                                    ->end()
170
                                    ->scalarNode('connection')
171
                                        ->defaultNull()
172
                                        ->info('The connection name, if applicable (e.g. Doctrine connection name). If not set, the default connection will be used.')
173
                                    ->end()
174
                                    ->arrayNode('config')
175
                                        ->info('Arbitrary configuration options, supported by your search engine')
176
                                        ->useAttributeAsKey('key')
177
                                        ->prototype('variable')->end()
178
                                    ->end()
179
                                ->end()
180
                            ->end()
181
                            ->arrayNode('fields_groups')
182
                                ->info('Definitions of fields groups.')
183
                                ->children()
184
                                    ->arrayNode('list')->prototype('scalar')->end()->end()
185
                                    ->scalarNode('default')->defaultValue('%ezsettings.default.content.field_groups.default%')->end()
186
                                ->end()
187
                            ->end()
188
                            ->arrayNode('options')
189
                                ->info('Options for repository.')
190
                                ->children()
191
                                    ->scalarNode('default_version_archive_limit')
192
                                        ->defaultValue(5)
193
                                        ->info('Default version archive limit (0-50), only enforced on publish, not on un-publish.')
194
                                    ->end()
195
                                ->end()
196
                            ->end()
197
                        ->end()
198
                    ->end()
199
                ->end()
200
            ->end();
201
    }
202
203
    public function addSiteaccessSection(ArrayNodeDefinition $rootNode)
204
    {
205
        $rootNode
206
            ->children()
207
                ->arrayNode('siteaccess')
208
                    ->info('SiteAccess configuration')
209
                    ->children()
210
                        ->arrayNode('list')
211
                            ->info('Available SiteAccess list')
212
                            ->example(array('ezdemo_site', 'ezdemo_site_admin'))
213
                            ->isRequired()
214
                            ->requiresAtLeastOneElement()
215
                            ->prototype('scalar')->end()
216
                        ->end()
217
                        ->arrayNode('groups')
218
                            ->useAttributeAsKey('key')
219
                            ->info('SiteAccess groups. Useful to share settings between Siteaccess')
220
                            ->example(array('ezdemo_group' => array('ezdemo_site', 'ezdemo_site_admin')))
221
                            ->prototype('array')
222
                                ->requiresAtLeastOneElement()
223
                                ->prototype('scalar')->end()
224
                            ->end()
225
                        ->end()
226
                        ->scalarNode('default_siteaccess')->isRequired()->info('Name of the default siteaccess')->end()
227
                        ->arrayNode('match')
228
                            ->info('Siteaccess match configuration. First key is the matcher class, value is passed to the matcher. Key can be a service identifier (prepended by "@"), or a FQ class name (prepended by "\\")')
229
                            ->example(
230
                                array(
231
                                    'Map\\URI' => array(
232
                                        'foo' => 'ezdemo_site',
233
                                        'ezdemo_site' => 'ezdemo_site',
234
                                        'ezdemo_site_admin' => 'ezdemo_site_admin',
235
                                    ),
236
                                    'Map\\Host' => array(
237
                                        'ezpublish.dev' => 'ezdemo_site',
238
                                        'admin.ezpublish.dev' => 'ezdemo_site_admin',
239
                                    ),
240
                                    '\\My\\Custom\\Matcher' => array(
241
                                        'some' => 'configuration',
242
                                    ),
243
                                    '@my.custom.matcher' => array(
244
                                        'some' => 'other_configuration',
245
                                    ),
246
                                )
247
                            )
248
                            ->isRequired()
249
                            ->useAttributeAsKey('key')
250
                            ->normalizeKeys(false)
251
                            ->prototype('array')
252
                                ->useAttributeAsKey('key')
253
                                ->beforeNormalization()
254
                                    ->always(
255
                                        function ($v) {
256
                                            // Value passed to the matcher should always be an array.
257
                                            // If value is not an array, we transform it to a hash, with 'value' as key.
258
                                            if (!is_array($v)) {
259
                                                return array('value' => $v);
260
                                            }
261
262
                                            // If passed value is a numerically indexed array, we must convert it into a hash.
263
                                            // See https://jira.ez.no/browse/EZP-21876
264
                                            if (array_keys($v) === range(0, count($v) - 1)) {
265
                                                $final = array();
266
                                                foreach ($v as $i => $val) {
267
                                                    $final["i$i"] = $val;
268
                                                }
269
270
                                                return $final;
271
                                            }
272
273
                                            return $v;
274
                                        }
275
                                    )
276
                                ->end()
277
                                ->normalizeKeys(false)
278
                                ->prototype('variable')->end()
279
                            ->end()
280
                        ->end()
281
                    ->end()
282
                    ->beforeNormalization()
283
                        ->always()->then(function ($v) {
284
                            if (isset($this->siteAccessConfigurationFilters)) {
285
                                foreach ($this->siteAccessConfigurationFilters as $filter) {
286
                                    $v = $filter->filter($v);
287
                                }
288
                            }
289
290
                            return $v;
291
                        })
292
                    ->end()
293
                ->end()
294
                ->arrayNode('locale_conversion')
295
                    ->info('Locale conversion map between eZ Publish format (i.e. fre-FR) to POSIX (i.e. fr_FR). The key is the eZ Publish locale. Check locale.yml in EzPublishCoreBundle to see natively supported locales.')
296
                    ->example(array('fre-FR' => 'fr_FR'))
297
                    ->useAttributeAsKey('key')
298
                    ->normalizeKeys(false)
299
                    ->prototype('scalar')->end()
300
                ->end()
301
            ->end();
302
    }
303
304
    private function addImageMagickSection(ArrayNodeDefinition $rootNode)
305
    {
306
        $filtersInfo =
307
<<<EOT
308
DEPRECATED.
309
This is only used for legacy injection.
310
You may use imagick/gmagick liip_imagine bundle drivers.
311
312
Hash of filters to be used for your image variations config.
313
#   Key is the filter name, value is an argument passed to "convert" binary.
314
#   You can use numbered placeholders (aka input variables) that will be replaced by defined parameters in your image variations config
315
EOT;
316
317
        $rootNode
318
            ->children()
319
                ->arrayNode('imagemagick')
320
                    ->info('ImageMagick configuration')
321
                    ->children()
322
                        ->booleanNode('enabled')->defaultTrue()->end()
323
                        ->scalarNode('path')
324
                            ->info('Absolute path of ImageMagick / GraphicsMagick "convert" binary.')
325
                            ->beforeNormalization()
326
                                ->ifTrue(
327
                                    function ($v) {
328
                                        $basename = basename($v);
329
                                        // If there is a space in the basename, just drop it and everything after it.
330
                                        if (($wsPos = strpos($basename, ' ')) !== false) {
331
                                            $basename = substr($basename, 0, $wsPos);
332
                                        }
333
334
                                        return !is_executable(dirname($v) . DIRECTORY_SEPARATOR . $basename);
335
                                    }
336
                                )
337
                                ->thenInvalid('Please provide full path to ImageMagick / GraphicsMagick  "convert" binary. Please also check that it is executable.')
338
                            ->end()
339
                        ->end()
340
                        ->arrayNode('filters')
341
                            ->info($filtersInfo)
342
                            ->example(array('geometry/scaledownonly' => '"-geometry {1}x{2}>"'))
343
                            ->prototype('scalar')->end()
344
                        ->end()
345
                    ->end()
346
                ->end()
347
            ->end();
348
    }
349
350
    private function addHttpCacheSection(ArrayNodeDefinition $rootNode)
351
    {
352
        $purgeTypeInfo = <<<EOT
353
Http cache purge type.
354
355
Cache purge for content/locations is triggered when needed (e.g. on publish) and will result in one or several Http PURGE requests.
356
Can be "local", "http" or a valid symfony service id:
357
- If "local" is used, an Http PURGE request will be emulated when needed (e.g. when using Symfony internal reverse proxy).
358
- If "http" is used, a full HTTP PURGE/BAN is done to a real reverse proxy (Varnish, ..) depending on your config
359
- If custom symfony service id is used, then check documentation on that service for how it behaves and how you need to configure your system for it.
360
361
If ezplatform-http-cache package is enabled (default as of 1.12 and up), then go to documentation on this package for further
362
info on how it supports multiple response tagging, purges and allow plugins for custom purge types.
363
364
If that is not enabled, then the (deprecated as of 1.8) default BAN based system will be used instead.
365
Where ressponses can be tagged by a single  X-Location-Id header, and for purges a single Http BAN request will be sent,
366
where X-Location-Id header consists of a Regexp containing locationIds to ban.
367
  BAN Examples:
368
   - (123|456|789) => Purge locations #123, #456, #789.
369
   - .* => Purge all locations.
370
EOT;
371
372
        $rootNode
373
            ->children()
374
                ->arrayNode('http_cache')
375
                    ->children()
376
                        ->scalarNode('purge_type')
377
                            ->info($purgeTypeInfo)
378
                            ->defaultValue('local')
379
                            ->beforeNormalization()
380
                                ->ifTrue(
381
                                    function ($v) {
382
                                        $http = array('multiple_http' => true, 'single_http' => true);
383
384
                                        return isset($http[$v]);
385
                                    }
386
                                )
387
                                ->then(
388
                                    function () {
389
                                        return 'http';
390
                                    }
391
                                )
392
                            ->end()
393
                        ->end()
394
                        ->scalarNode('timeout')->info('DEPRECATED')->end()
395
                    ->end()
396
                ->end()
397
            ->end();
398
    }
399
400
    private function addPageSection(ArrayNodeDefinition $rootNode)
401
    {
402
        $pageInfo = <<<EOT
403
List of globally registered layouts and blocks used by the Page fieldtype
404
EOT;
405
406
        $rootNode
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Symfony\Component\Config...\Builder\NodeDefinition as the method children() does only exist in the following sub-classes of Symfony\Component\Config...\Builder\NodeDefinition: Symfony\Component\Config...der\ArrayNodeDefinition. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
407
            ->children()
408
                ->arrayNode('ezpage')
409
                    ->info($pageInfo)
410
                    ->children()
411
                        ->arrayNode('layouts')
412
                            ->info('List of registered layouts, the key is the identifier of the layout')
413
                            ->useAttributeAsKey('key')
414
                            ->normalizeKeys(false)
415
                            ->prototype('array')
416
                                ->children()
417
                                    ->scalarNode('name')->isRequired()->info('Name of the layout')->end()
418
                                    ->scalarNode('template')->isRequired()->info('Template to use to render this layout')->end()
419
                                ->end()
420
                            ->end()
421
                        ->end()
422
                        ->arrayNode('blocks')
423
                            ->info('List of registered blocks, the key is the identifier of the block')
424
                            ->useAttributeAsKey('key')
425
                            ->normalizeKeys(false)
426
                            ->prototype('array')
427
                                ->children()
428
                                    ->scalarNode('name')->isRequired()->info('Name of the block')->end()
429
                                ->end()
430
                            ->end()
431
                        ->end()
432
                        ->arrayNode('enabledBlocks')
433
                            ->prototype('scalar')
434
                            ->end()
435
                            ->info('List of enabled blocks by default')
436
                        ->end()
437
                        ->arrayNode('enabledLayouts')
438
                            ->prototype('scalar')
439
                            ->end()
440
                            ->info('List of enabled layouts by default')
441
                        ->end()
442
                    ->end()
443
                ->end()
444
            ->end();
445
    }
446
447
    private function addRouterSection(ArrayNodeDefinition $rootNode)
448
    {
449
        $nonSAAwareInfo = <<<EOT
450
Route names that are not supposed to be SiteAccess aware, i.e. Routes pointing to asset generation (like assetic).
451
Note that you can just specify a prefix to match a selection of routes.
452
e.g. "_assetic_" will match "_assetic_*"
453
Defaults to ['_assetic_', '_wdt', '_profiler', '_configurator_']
454
EOT;
455
        $rootNode
456
            ->children()
457
                ->arrayNode('router')
458
                    ->children()
459
                        ->arrayNode('default_router')
460
                            ->children()
461
                                ->arrayNode('non_siteaccess_aware_routes')
462
                                    ->prototype('scalar')->end()
463
                                    ->info($nonSAAwareInfo)
464
                                    ->example(array('my_route_name', 'some_prefix_'))
465
                                ->end()
466
                            ->end()
467
                        ->end()
468
                    ->end()
469
                    ->info('Router related settings')
470
                ->end()
471
            ->end();
472
    }
473
474
    /**
475
     * Define global Semantic Configuration for RichText.
476
     *
477
     * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $rootNode
478
     */
479
    private function addRichTextSection(ArrayNodeDefinition $rootNode)
480
    {
481
        $this->addCustomTagsSection(
482
            $rootNode->children()->arrayNode('ezrichtext')->children()
483
        )->end()->end()->end();
484
    }
485
486
    /**
487
     * Define RichText Custom Tags Semantic Configuration.
488
     *
489
     * The configuration is available at:
490
     * <code>
491
     * ezpublish:
492
     *     ezrichtext:
493
     *         custom_tags:
494
     * </code>
495
     *
496
     * @param \Symfony\Component\Config\Definition\Builder\NodeBuilder $ezRichTextNode
497
     *
498
     * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition
499
     */
500
    private function addCustomTagsSection(NodeBuilder $ezRichTextNode)
501
    {
502
        return $ezRichTextNode
503
                ->arrayNode('custom_tags')
504
                // workaround: take into account Custom Tag names when merging configs
505
                ->useAttributeAsKey('tag')
506
                ->arrayPrototype()
507
                    ->children()
508
                        ->scalarNode('template')
509
                            ->isRequired()
510
                        ->end()
511
                        ->scalarNode('icon')
512
                            ->defaultNull()
513
                        ->end()
514
                        ->arrayNode('attributes')
515
                            ->useAttributeAsKey('attribute')
516
                            ->isRequired()
517
                            ->arrayPrototype()
518
                                ->beforeNormalization()
519
                                    ->always(
520
                                        function ($v) {
521
                                            // Workaround: set empty value to be able to unset it later on (see validation for "choices")
522
                                            if (!isset($v['choices'])) {
523
                                                $v['choices'] = [];
524
                                            }
525
526
                                            return $v;
527
                                        }
528
                                    )
529
                                ->end()
530
                                ->validate()
531
                                    ->ifTrue(
532
                                        function ($v) {
533
                                            return $v['type'] === 'choice' && !empty($v['required']) && empty($v['choices']);
534
                                        }
535
                                    )
536
                                    ->thenInvalid('List of choices for required choice type attribute has to be non-empty')
537
                                ->end()
538
                                ->validate()
539
                                    ->ifTrue(
540
                                        function ($v) {
541
                                            return !empty($v['choices']) && $v['type'] !== 'choice';
542
                                        }
543
                                    )
544
                                    ->thenInvalid('List of choices is supported by choices type only.')
545
                                ->end()
546
                                ->children()
547
                                    ->enumNode('type')
548
                                        ->isRequired()
549
                                        ->values(static::CUSTOM_TAG_ATTRIBUTE_TYPES)
550
                                    ->end()
551
                                    ->booleanNode('required')
552
                                        ->defaultFalse()
553
                                    ->end()
554
                                    ->scalarNode('default_value')
555
                                        ->defaultNull()
556
                                    ->end()
557
                                    ->arrayNode('choices')
558
                                        ->scalarPrototype()->end()
559
                                        ->performNoDeepMerging()
560
                                        ->validate()
561
                                            ->ifEmpty()->thenUnset()
562
                                        ->end()
563
                                    ->end()
564
                                ->end()
565
                            ->end()
566
                        ->end()
567
                    ->end()
568
                ->end()
569
            ->end()
570
        ;
571
    }
572
573
    /**
574
     * Define Url Alias Slug converter Semantic Configuration.
575
     *
576
     * The configuration is available at:
577
     * <code>
578
     * ezpublish:
579
     *     url_alias:
580
     *         slug_converter:
581
     *             transformation: name_of_transformation_group_to_use
582
     *             separator:  name_of_separator_to_use
583
     *             transformation_groups:
584
     *                 transformation_group_name: name of existing or new transformation group
585
     *                     commands : [] array of commands which will be added to group
586
     *                     cleanup_method: name_of_cleanup_method
587
     * </code>
588
     *
589
     * @param \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition $rootNode
590
     *
591
     * @return \Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition
592
     */
593
    private function addUrlAliasSection(ArrayNodeDefinition $rootNode)
594
    {
595
        return $rootNode
596
            ->children()
597
                ->arrayNode('url_alias')
598
                    ->children()
599
                        ->arrayNode('slug_converter')
600
                            ->children()
601
                                ->scalarNode('transformation')->end()
602
                                ->scalarNode('separator')->end()
603
                                ->arrayNode('transformation_groups')
604
                                    ->arrayPrototype()
605
                                        ->children()
606
                                            ->arrayNode('commands')
607
                                                ->scalarPrototype()->end()
608
                                            ->end()
609
                                            ->scalarNode('cleanup_method')->end()
610
                                        ->end()
611
                                    ->end()
612
                                ->end()
613
                            ->end()
614
                        ->end()
615
                    ->end()
616
                ->end()
617
            ->end();
618
    }
619
}
620