Symfony configuration validation of keys in key-value pairs in prototype node

Viewed 958

In Symfony 2.8, given this YAML config block

my_bundle:
  profiles:
    'abc': 123
    'def: 456
    '...': ...

How can I validate the keys in the key-values under profiles are non-empty string?

In this case 'abc' and 'def' are just examples, these are arbitrary, they cannot be pre-defined.

Here's my Configuration file -

->arrayNode('profiles')
    ->isRequired()
    ->requiresAtLeastOneElement()
    ->useAttributeAsKey('name')
    ->prototype('integer')
        ->min(0)
    ->end()
->end()

The problem is

my_bundle:
  profiles:
    123
    456

is valid when I don't want it to be.

I could not find an easy in-built way to validate keys of prototype arrays.

Possible solutions

One: Seems that I could make it an array of arrays

my_bundle:
  profiles:
    -
      name: abc
      value: 123
    -
      name: def
      value: 456

with

->arrayNode('profiles')
    ->isRequired()
    ->useAttributeAsKey('name')
    ->requiresAtLeastOneElement()
    ->prototype('array')
        ->children()
            ->scalarNode('name')
                ->isRequired()
                ->cannotBeEmpty()
                ->validate()
                    ->ifTrue(
                        function($s) {
                            return !is_string($s);
                        }
                    )
                    ->thenInvalid('Profile key must be a non-empty string')
                ->end()
            ->end()
            ->integerNode('value')
                ->isRequired()
                ->min(0)
            ->end()
        ->end()
    ->end()

but I'd prefer not to do that as our customers already have the original format in their config, we can more easily update the Configuration than the config files themselves.

Two: Seems that I could do #1 without changing the config by adding some normalisation to convert the old config format to a new format first

->arrayNode('profiles')
    ->beforeNormalization()
        ->ifTrue(
            function($array) {
                $hasSimpleKeyValuePair = false;
                foreach ($array as $key => $value) {
                    if (!is_array($value)) {
                        $hasSimpleKeyValuePair = true;
                        break;
                    }
                }

                return $hasSimpleKeyValuePair;
            }
        )
        ->then(
            function($array) {
                $newArray = [];
                foreach ($array as $key => $value) {
                    if (!is_array($value)) {
                        $newArray[] = ['name' => $key, 'value' => $value];
                    }
                }
                return $newArray;
            }
        )
    ->end()
    ->isRequired()
    ->requiresAtLeastOneElement()
    ->prototype('array')
        ->children()
            ->scalarNode('name')
                ->isRequired()
                ->cannotBeEmpty()
                ->validate()
                    ->ifTrue(
                        function($s) {
                            return !is_string($s);
                        }
                    )
                    ->thenInvalid('Profile key must be a non-empty string')
                ->end()
            ->end()
            ->integerNode('value')
                ->isRequired()
                ->min(0)
            ->end()
        ->end()
    ->end()
->end()

but that seems like a massive overkill.

(These two options also mean I need to change the receivers of the profile data to know that rather than using the number, they need to use an array key index that points to the number, $profile[$key]['value'] rather than just $profile[$key], which I can do I guess)

Three: Seems that I could add a custom validation on the whole block

->arrayNode('profiles')
    ->isRequired()
    ->requiresAtLeastOneElement()
    ->useAttributeAsKey('name')
    ->prototype('integer')
        ->min(0)
    ->end()
    ->validate()
        ->ifTrue(function ($array) {
            $notValid = false;
            foreach ($array as $key => $value) {
                if (!is_string($key) || empty($key)) {
                    $notValid = true;
                    break;
                }
            }

            return $notValid;
        })
        ->thenInvalid('Profile keys must be a non-empty string.')
    ->end()
->end()

which is what I'm going with for now, but still seems unnecessary custom/hacky for something I think would be a common use-case.

I just want to check I've not missed some magical in-built way to do this with Symfony configuration?

0 Answers
Related