在 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
可以很好的解决(因为我看是什么字段,
price_category
无法解决(第二条消息)。
{
"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).
我有一个内置于 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
可以很好的解决(因为我看是什么字段,price_category
无法解决(第二条消息)。
{
"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).