【问题标题】:Symfony Configuration: how to differentiate validation based on the value set in the same node levelSymfony 配置:如何根据同一节点级别中设置的值来区分验证
【发布时间】:2017-01-05 11:31:16
【问题描述】:

我正在写 a bundle to manage feature prices and plans,我有一个复杂的配置需要验证。

实际上我会区分CountableFeatures 和RechargeableFeatures。

这是两者的配置示例:

features:
    the_name_of_features_set:
        features:
            a_rechargeable_feature:
                type: rechargeable
                # The free amount to recharge the first time (or when you like)
                free_recharge: 10
                cumulable: true
                unitary_price:
                    EUR: 100
                packs:
                    10:
                        EUR: 1000
                    50:
                        EUR: 5000
                    100:
                        EUR: 10000
                    500:
                        EUR: 50000
                    1000:
                        EUR: 100000
            a_countable_feature:
                type: countable
                cumulable: true
                unitary_price:
                    EUR:
                        monthly: 1000
                        yearly: 10000
                packs:
                    10: ~ # <- This is free!
                    50:
                        EUR:
                            monthly: 500
                            yearly: 5000
                    100:
                        EUR:
                            monthly: 1000
                            yearly: 10000
                    500:
                        EUR:
                            monthly: 5000
                            yearly: 50000
                    1000:
                        EUR:
                            monthly: 50000
                            yearly: 500000

如您所见,这两种功能非常相似。 唯一的区别是价格结构CountableFeatures 可以订阅,因此价格必须考虑订阅期(每月或每年)RechargeableFeatures 会在用户请求时进行充值,因此在购买功能充值时他们只需支付 unatantum 价格

但两者的配置非常相似,并且具有相同的值(cumulableunitary_pricepacks)。

因此,在验证价格配置时,我必须考虑到,如果验证路径是 ...a_COUNTABLE_FEATURE.UNITARY_PRICE,则价格结构必须预期为由年度和每月间隔组成,而如果验证路径为...a_RECHARGEABLE_FEATURE.UNITARY_PRICE 价格简单,没有订阅期。

特征类型在type 节点中定义,在...a_RECHARGEABLE_FEATURE.type 下,与unitary price 处于同一级别。

当我在 unitary_price 中进行验证时,如何读取 type 的值,以便对不同类型的功能进行不同的验证? 或者,我如何构建验证TreeBuilder 考虑到这一点?

  1. 我可以创建两个单独的方法:validateUntantumPrice()validateSubscribtionPrice(),但是如何根据 type 的值调用其中一个或另一个?
  2. 或者我怎样才能让他们知道type 的值,以便我可以根据它的值进行验证,直接在方法中检查它?
  3. 我还可以在Configuration 类本身的属性中设置type 值,但是在验证type 时如何设置它?

同样适用于packs 的配置值:如果是可充电功能包,价格很简单 (unatantum),而如果是可计数功能包,价格有订阅期。

这是我想出的配置。这一直有效,直到我引入了新的 CountableFeature 类型,现在我无法继续前进。

class Configuration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('features');

        $rootNode
            ->useAttributeAsKey('name')
            ->prototype('array')
                ->children()
                    ->arrayNode('features')
                    ->useAttributeAsKey('name')
                        ->prototype('array')
                            ->children()
                                ->enumNode('type')->values(['boolean', 'countable', 'rechargeable'])->isRequired()->cannotBeEmpty()->end()
                                // @todo Only if type === Boolean
                                ->scalarNode('enabled')->defaultFalse()->end()
                                // @todo Only if type === Rechargeable
                                ->scalarNode('cumulable')->defaultFalse()->end()
                                // @todo Only if type === Rechargeable
                                ->scalarNode('free_recharge')->defaultNull()->end()
                                // @todo Only if type === Rechargeable
                                ->arrayNode('unitary_price')
                                    // @todo Validate currency code
                                    ->useAttributeAsKey('name')
                                    ->prototype('integer')->end()
                                ->end()
                                // @todo Only if type === Rechargeable
                                ->arrayNode('packs')
                                    ->useAttributeAsKey('name')
                                    ->prototype('array')
                                        // @todo Validate currency code
                                        ->useAttributeAsKey('name')
                                        ->prototype('integer')->end()
                                    ->end()
                                ->end()
                                ->arrayNode('prices')
                                    // @todo Validate currency code
                                    ->useAttributeAsKey('name')
                                    ->prototype('array')
                                        ->children()
                                            // @todo Set this as section
                                            ->scalarNode('monthly')->defaultNull()->end()
                                            ->scalarNode('yearly')->defaultNull()->end()
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                        ->end()
                    ->end() // End features
                ->end()
            ->end();

        return $treeBuilder;
    }
}

【问题讨论】:

  • 是的,我做到了,但我不知道如何上一级然后继续验证,因为... 尝试编写验证:你会明白我在说什么。 ..我必须先到达节点... a_rechargeable_feature.type,然后根据它的值,验证同一级别上的以下节点... a_rechargeable_feature.unitary_price... a_rechargeable_feature.packs。但是,一旦我到达... a_rechargeable_feature.type,我怎样才能上一层并继续验证?
  • 我应该创建两个单独的方法:validateUntantumPrice()validateSubscribtionPrice() 但是如何根据 type 的值调用其中一个或另一个?
  • 您需要在type 级别分叉它。根据类型附加一种带参数的方法或不同的方法。
  • 你能写一个例子吗?

标签: php validation symfony


【解决方案1】:

我去掉了packs 以使其更具可读性,因为它非常罗嗦。

用于配置yaml

features:
    the_name_of_features_set:
        features:
            a_rechargeable_feature:
                type: rechargeable
                free_recharge: 10
                cumulable: true
                unitary_price:
                    EUR: 100
            a_countable_feature:
                type: countable
                cumulable: true
                unitary_price:
                    EUR:
                        monthly: 1000
                        yearly: 10000

条件配置定义可以是这样的:

class Configuration implements ConfigurationInterface
{
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('features');

        $rootNode
            ->useAttributeAsKey('name')
            ->prototype('array')
                ->children()
                    ->arrayNode('features')
                    ->useAttributeAsKey('name')
                        ->prototype('array')
                            ->children()
                                ->enumNode('type')
                                    ->values(['boolean', 'countable', 'rechargeable'])
                                    ->isRequired()
                                    ->cannotBeEmpty()
                                ->end()
                                ->scalarNode('cumulable')->defaultFalse()->end()
                                ->scalarNode('free_recharge')->defaultNull()->end()
                                ->arrayNode('unitary_price')
                                    ->useAttributeAsKey('name')
                                    ->prototype('array')
                                        // here we convert 'EUR'=>100 to 'EUR'=>['_'=>100] to make it an array as declared
                                        ->beforeNormalization()
                                            ->ifTrue(function($v) {return is_numeric($v);})
                                            ->then(function ($v) {
                                                return array('_' => $v);
                                            })
                                            ->end()
                                        ->children()
                                            /// here we define acceptable keys for all types, including the artificial one '_' for scalars
                                            ->scalarNode('monthly')->defaultNull()->end()
                                            ->scalarNode('yearly')->defaultNull()->end()
                                            ->scalarNode('_')->defaultNull()->end()
                                        ->end()
                                    ->end()
                                ->end()
                            ->end()
                            // add validation rules
                            ->validate()
                                ->ifTrue(function($feature) {
                                    return $this->validateFeature($feature);
                                })
                                // tidy up and convert rechargeable price back to scalar
                                ->then(function($feature) {
                                    return $this->processFeature($feature);
                                })
                            ->end()
                        ->end()
                    ->end() // End features
                ->end()
            ->end();

        return $treeBuilder;
    }

    // this part should be self-explanatory

    // validate feature depending on type
    protected function validateFeature($feature)
    {
        switch ($feature['type']) {
            case 'rechargeable':
                $ok = $this->validateRechargeable($feature['unitary_price']);
                break;
            case 'countable':
                $ok = $this->validateCountable($feature['unitary_price']);
                break;
            default:
                // it shouldn't be reachable because of enum type, but you need to handle boolean as well
                $ok = false;
        }
        if(!$ok) {
            throw new \InvalidArgumentException('Invalid configuration for ' . json_encode($feature));
        }
        return true;
    }


    // all rechargeables should have scalar '_' type
    protected function validateRechargeable($unitaryPrice)
    {
        return array_reduce(
            $unitaryPrice,
            function($result, $price) {
                return $result && is_numeric($price['_']);
            },
            true
        );
    }

    // all countables should not have '_' scalar type
    protected function validateCountable($unitaryPrice)
    {
        return array_reduce(
            $unitaryPrice,
            function($result, $price) {
                return $result && $price['_'] === null;
            },
            true
        );
    }

    // revert changes depending on type
    protected function processFeature($feature)
    {
        switch ($feature['type']) {
            case 'rechargeable':
                return $this->processRechargeable($feature['unitary_price']);
            case 'countable':
                return $this->processCountable($feature['unitary_price']);
            default:
                // again, not sure what boolean type should do
                throw new \InvalidArgumentException('Unsupported feature type ' . $feature['type']);
        }
    }

    // convert rechargeable arrays back to scalar
    protected function processRechargeable($unitaryPrice)
    {
        array_walk(
            $unitaryPrice,
            function(&$price){
                $price = $price['_'];
            });
        return $unitaryPrice;
    }

    // remove injected '_' from countable arrays
    protected function processCountable($unitaryPrice)
    {
        array_walk(
            $unitaryPrice,
            function(&$price){
                unset($price['_']);
            });
        return $unitaryPrice;
    }
}

【讨论】:

猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-02
  • 2019-10-25
  • 1970-01-01
  • 2020-05-19
相关资源
最近更新 更多