在 Symfony API 中自动匹配 property_path

automatching property_path in Symfony API

我有一个内置于 Symfony3 的 REST-API。

这里的示例是使用 FormBuilderInterface 制作的表单中价格的 API 字段。下面的代码示例是 ApiBundle/Form/PriceType.php

class PriceType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('name', TextType::class, array(
                  'description' => 'Name',

              ))
             ->add('price_category', EntityPublicKeyTextType::class, array(
                'class' => 'MyCustomBundle:PriceCategory',
                'property_path' => 'priceCategory',
            ))

问题是关于字段的良好响应消息,例如验证错误。对于默认的 symfony 类型(例如 IntegerType、TextType),它可以自动找到 property_path 并给我一个有用的错误消息。这是有两个错误的 API 响应:

    {
      "name": [
        "This value is too long. It should have 50 characters or less."
      ],
      "0": "This value should not be null."
    }

解决问题。我为字段 price_category 添加 'property_path' => 'priceCategory'。 property_path 的值与 BaseBundle/Entity/Price.php 匹配,其中定义了 var protected $priceCategory;

添加 property_path 后,错误消息看起来正常。

{
  "name": [
    "This value is too long. It should have 50 characters or less."
  ],
  "price_category": [
    "This value should not be null."
  ]
}

price_category 的 class 是 EntityPublicKeyTextType ,它是从 TextType 中抽象出来的(它可以很好地处理错误)。

因此我有以下问题:我必须向继承的 class EntityPublicKeyTextType 添加什么以避免手动为所有字段添加 property_path?

非常欢迎任何解决此问题的提示

最好的内窥镜

编辑:

EntityPublicKeyTextType:

class EntityPublicKeyTextType extends AbstractType
{
    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @param ObjectManager $om
     */
    public function __construct(ObjectManager $om)
    {
        $this->om = $om;
    }

    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $transformer = new ObjectToPublicKeyTransformer(
            $this->om,
            $options['class'],
            $options['public_key'],
            $options['remove_whitespaces'],
            $options['multiple'],
            $options['string_separator'],
            $options['extra_find_by']
        );
        $builder->addModelTransformer($transformer);
    }

    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver
            ->setRequired(array(
                'class',
                'public_key'
            ))
            ->setDefaults(array(
                'multiple' => false,
                'string_separator' => false,
                'extra_find_by' => array(),
                'remove_whitespaces' => true,
            ));
    }

    public function getParent()
    {
        return TextType::class;
    }

    public function getBlockPrefix()
    {
        return 'entity_public_key_text';
    }
}

ObjectToPublicKeyTransformer:

class ObjectToPublicKeyTransformer implements DataTransformerInterface
{
    /**
     * @var PropertyAccessorInterface
     */
    private $propertyAccessor;

    /**
     * @var ObjectManager
     */
    private $om;

    /**
     * @var string
     */
    private $class;

    /**
     * @var string|string[]
     */
    private $publicKey;

    /**
     * @var bool
     */
    private $removeWhitespaces;

    /**
     * @var boolean
     */
    private $multiple;

    /**
     * @var boolean|string
     */
    private $stringSeparator;

    /**
     * @var array
     */
    private $extraFindBy;

    public function __construct(
        ObjectManager $om,
        string $class,
        $publicKey,
        bool $removeWhitespaces,
        bool $multiple = false,
        $stringSeparator = false,
        array $extraFindBy = array(),
        PropertyAccessorInterface $propertyAccessor = null
    ) {
        $this->propertyAccessor = $propertyAccessor ?: PropertyAccess::createPropertyAccessor();
        $this->om = $om;
        $classMetadata = $om->getClassMetadata($class);
        $this->class = $classMetadata->getName();
        $this->publicKey = $publicKey;
        $this->stringSeparator = $stringSeparator;
        $this->multiple = $multiple;
        $this->extraFindBy = $extraFindBy;
        $this->removeWhitespaces = $removeWhitespaces;
    }

    /**
     * Transforms an object / Collection of objects to a publicKey string / array of publicKey strings.
     *
     * @param   object|Collection $object
     * @return  string|array
     */
    public function transform($object)
    {
        if (null == $object) {
            return null;
        }

        if (is_array($this->publicKey)) {
            $publicKey = $this->publicKey[0];
        } else {
            $publicKey = $this->publicKey;
        }

        if ($this->multiple) {
            if ($object instanceof Collection) {
                $values = array();

                foreach ($object as $objectItem) {
                    $values[] = (string)$this->propertyAccessor->getValue($objectItem, $publicKey);
                }

                if ($this->stringSeparator) {
                    return implode($this->stringSeparator, $values);
                }

                return $values;
            }
        } else {
            return (string)$this->propertyAccessor->getValue($object, $publicKey);
        }
    }

    /**
     * Transforms an publicKey string / array of public key strings to an object / Collection of objects.
     *
     * @param   string|array $value
     * @return  object|Collection
     *
     * @throws TransformationFailedException if object is not found.
     */
    public function reverseTransform($value)
    {
        if (null === $value) {
            return $this->multiple ? new ArrayCollection() : null;
        }

        if (is_array($this->publicKey)) {
            $publicKeys = $this->publicKey;
        } else {
            $publicKeys = array($this->publicKey);
        }

        if ($this->multiple) {
            if ($this->stringSeparator) {
                $value = explode($this->stringSeparator, $value);
            }

            if (is_array($value)) {
                $objects = new ArrayCollection();

                foreach ($value as $valueItem) {
                    foreach ($publicKeys as $publicKey) {
                        $object = $this->findObject($valueItem, $publicKey);

                        if ($object instanceof $this->class) {
                            $objects->add($object);
                            break;
                        }
                    }
                }

                return $objects;
            }
        }

        foreach ($publicKeys as $publicKey) {
            $object = $this->findObject($value, $publicKey);

            if ($object instanceof $this->class) {
                return $object;
            }
        }

        return $this->multiple ? new ArrayCollection() : null;
    }

    private function findObject($value, $publicKey)
    {
        if ($this->removeWhitespaces) {
            $value = str_replace(' ', '', $value);
        }
        $findBy = array_merge([$publicKey => $value], $this->extraFindBy);
        $object = $this->om->getRepository($this->class)->findOneBy($findBy);

        return $object;
    }
}

如果您也提供 Price model/entity class,将会很有用。您似乎在模型 (priceCategory) 中对 属性 名称使用了驼峰式大小写,然后在您的表单中使用了蛇形大小写 (price_category)。

如果您对模型和表单使用相同的约定,验证错误将自动映射到正确的 属性。

解释是 Symfony 的映射器仍然可以通过将 snake 转换为驼峰大小写来映射您的字段,反之亦然,这就是为什么即使不使用 property_path 选项,您的表单仍然可以工作并提交值的原因。但问题是验证器没有做这个映射,无法匹配正确的 属性 (price_category -> priceCategory).