>我有一个表单,用户在其中输入他的电话号码。一个常见的问题是电话号码可以用许多不同的方式书写:"+49 711 XXXXXX"、"0049 (0)711 XXXXXX"或"+49 711 - XXXXXX"都是同一电话号码的表示形式。为了检测重复项,我使用"电话号码捆绑包"(https://github.com/misd-service-development/phone-number-bundle)来获取可用于比较的电话号码的"规范化"E.164表示形式。如果检测到重复,则不得存储输入的号码,并且必须向用户显示通知。
如果输入的电话号码是有效的电话号码,我想检查电话号码的 E.164 格式值是否已存储在数据库表中。
这是电话号码的 MySQL 表:
-+----+---------------------+----------------+
| id | original | phonenumber |
-+----+---------------------+----------------+
| 1 | 0711-xxxxxxx | +49711xxxxxxx |
-+----+---------------------+----------------+
| 2 | +49 7034 / xxxxx-xx | +497034xxxxxxx |
-+----+---------------------+----------------+
| 3 | +49 (0)171/xxxxxxx | +49171xxxxxxx |
-+----+---------------------+----------------+
| .. | ... | ... |
-+----+---------------------+----------------+
"电话号码"包含在表单中输入的值的 E.164 格式值。最初输入的第一个值作为附加信息存储在"原始"列中。
表单在"src/AppBundle/Form/PhonenumberType.php"中定义:
<?php
namespace AppBundle'Form;
use Symfony'Component'Form'AbstractType;
use libphonenumber'PhoneNumberFormat;
use Symfony'Component'Form'FormBuilderInterface;
use Symfony'Component'OptionsResolver'OptionsResolver;
class PhonenumberType extends AbstractType
{
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
//->add('phonenumber') // Remove comments to see that the unique constraint works when the phonenumber is submitted via form
->add('original')
;
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle'Entity'Phonenumber'
));
}
}
电话号码实体"src/AppBundle/Entity/Phonenumber.php":
<?php
namespace AppBundle'Entity;
use Doctrine'ORM'Mapping as ORM;
use Symfony'Component'Validator'Constraints as Assert;
use AppBundle'Validator'Constraints as PhonenumberAssert;
use Symfony'Bridge'Doctrine'Validator'Constraints'UniqueEntity;
/**
* Phonenumber
*
* @ORM'Table(name="phonenumber",
* uniqueConstraints={
* @ORM'UniqueConstraint(columns={"phonenumber"})
* })
* @ORM'Entity
* @UniqueEntity("phonenumber")
* @ORM'HasLifecycleCallbacks()
*/
class Phonenumber
{
/**
* @var integer
*
* @ORM'Column(name="id", type="integer", precision=0, scale=0, nullable=false, unique=false)
* @ORM'Id
* @ORM'GeneratedValue(strategy="IDENTITY")
*/
private $id;
/**
* @var string
*
* @ORM'Column(name="phonenumber", type="string", length=255, unique=true)
*/
private $phonenumber;
/**
* @var string
*
* @ORM'Column(name="original", type="string", length=255, precision=0, scale=0, nullable=true, unique=false)
* @Assert'NotBlank
* @PhonenumberAssert'IsValidPhoneNumber
*/
private $original;
/**
* Constructor
*/
public function __construct()
{
}
/**
* Returns phonenumber.
*
* @return string
*/
public function __toString()
{
return $this->getPhonenumber();
}
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set phonenumber
*
* @param string $phonenumber
*
* @return Phonenumber
*/
public function setPhonenumber($phonenumber)
{
$this->phonenumber = $phonenumber;
return $this;
}
/**
* Get phonenumber
*
* @return string
*/
public function getPhonenumber()
{
return $this->phonenumber;
}
/**
* Set original
*
* @param string $original
*
* @return Phonenumber
*/
public function setOriginal($original)
{
$this->original = $original;
return $this;
}
/**
* Get original
*
* @return string
*/
public function getOriginal()
{
return $this->original;
}
}
在"app/config/services.yml"中定义的服务:
services:
phonenumber_validation:
class: AppBundle'Validator'Constraints'IsValidPhoneNumberValidator
arguments: ["@service_container"]
tags:
- { name: validator.constraint_validator, alias: phonenumber_validation }
my.subscriber:
class: AppBundle'EventListener'PhoneNumberNormalizerSubscriber
calls:
- [setContainer, ["@service_container"]]
tags:
- { name: doctrine.event_subscriber, connection: default }
订阅者类"src/AppBundle/EventListener/PhoneNumberNormalizerSubscriber.php":
<?php
namespace AppBundle'EventListener;
use Symfony'Component'DependencyInjection'ContainerInterface;
use Doctrine'Common'EventSubscriber;
use Doctrine'ORM'Event'LifecycleEventArgs;
use AppBundle'Entity'Phonenumber;
use libphonenumber'NumberParseException;
use libphonenumber'PhoneNumber;
use libphonenumber'PhoneNumberFormat;
use libphonenumber'PhoneNumberUtil;
class PhoneNumberNormalizerSubscriber implements EventSubscriber
{
/** @var ContainerInterface */
protected $container;
/**
* @param ContainerInterface @container
*/
public function setContainer(ContainerInterface $container)
{
$this->container = $container;
}
public function getSubscribedEvents()
{
return array(
'prePersist',
'preUpdate',
);
}
// Executed when data is stored for the first time
public function prePersist(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// only act on "Phonenumber" entity
if ($entity instanceof Phonenumber)
{
$entityManager = $args->getEntityManager();
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($entity->getOriginal(), 'DE');
$normalized_phonenumber = $this->container->get('libphonenumber.phone_number_util')->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhonenumber($normalized_phonenumber);
}
}
// Executed when data is already stored
public function preUpdate(LifecycleEventArgs $args)
{
$entity = $args->getEntity();
// only act on "Phonenumber" entity
if ($entity instanceof Phonenumber)
{
$entityManager = $args->getEntityManager();
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($entity->getOriginal(), 'DE');
$normalized_phonenumber = $this->container->get('libphonenumber.phone_number_util')->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhonenumber($normalized_phonenumber);
}
}
}
约束类"src/AppBundle/Validator/Constraint/IsValidPhoneNumber.php":
<?php
namespace AppBundle'Validator'Constraints;
use Symfony'Component'Validator'Constraint;
/**
* @Annotation
*/
class IsValidPhoneNumber extends Constraint
{
public $message_invalid = 'Not a valid phone number: "%string%"';
/**
* @return string
*/
public function validatedBy()
{
return 'phonenumber_validation';
}
}
验证器类 "src/AppBundle/Validator/Constraint/IsValidPhoneNumberValidator.php":
<?php
namespace AppBundle'Validator'Constraints;
use Symfony'Component'DependencyInjection'ContainerInterface as Container;
use Symfony'Component'Validator'Constraint;
use Symfony'Component'Validator'ConstraintValidator;
use libphonenumber'NumberParseException;
use libphonenumber'PhoneNumber;
use libphonenumber'PhoneNumberFormat;
use libphonenumber'PhoneNumberUtil;
class IsValidPhoneNumberValidator extends ConstraintValidator
{
private $container;
/**
* Construct
*/
public function __construct(Container $container)
{
$this->container = $container;
}
/**
* Validate
*
* @param mixed $value
* @param Constraint $constraint
*/
public function validate($value, Constraint $constraint)
{
if ($value != '' )
{
$phoneNumberObj = $this->container->get('libphonenumber.phone_number_util')->parse($value, 'DE');
if (!$this->container->get('libphonenumber.phone_number_util')->isValidNumber($phoneNumberObj))
{
$this->context->buildViolation($constraint->message_invalid)
->setParameter('%string%', $value)
->addViolation();
}
}
}
}
电话号码验证器有效 - 如果号码被"电话号码捆绑包"使号码无效,则会显示一条消息"不是有效的电话号码:"3333333333333333"。E.164 格式的值也会正确保存到数据库表中。
问题:尽管我对实体类中的$phonenumber属性使用"@ORM''UniqueConstraint(columns={"phonenumber"})","@UniqueEntity("phonenumber")"和"unique=true",但表单中输入的每个有效数字都会存储在数据库中,无论表中是否已经有重复项。当电话号码字段未添加到表单类型类中时,唯一约束不起作用。
可能很有趣: 当我删除电话号码类型类中的注释时,以便
->add('phonenumber')
再次包含并在关联的表单字段"电话号码"中输入现有号码,我得到"此值已被使用"。
我做错了什么?
感谢您的帮助!
事实上,事实并非如此。您必须区分:
- 表单验证。当您的控制器调用
$form->handle($request)
或类似的东西时,就会发生这种情况,这些调用在表单事件上触发 - 原则回调,在实体管理器
flush()
期间调用
解决方案是不是在prePersist()
上使用该捆绑包,而是在 Form 事件上使用该捆绑包。在将数据写入实体之前,将对数据进行规范化,然后有效地验证将保存到数据库的内容。
这样的代码如下所示:
<?php
namespace AppBundle'Form;
use Symfony'Component'Form'AbstractType;
use libphonenumber'PhoneNumberFormat;
use Symfony'Component'Form'FormBuilderInterface;
use Symfony'Component'OptionsResolver'OptionsResolver;
use Symfony'Component'Form'FormEvent;
use Symfony'Component'Form'FormEvents;
class PhonenumberType extends AbstractType
{
private $phoneNumberUtil;
public function __construct(PhoneNumberUtil $phoneNumberUtil)
{
$this->phoneNumberUtil = $phoneNumberUtil;
}
/**
* @param FormBuilderInterface $builder
* @param array $options
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('phonenumber', 'hidden')
->add('original')
->addEventListener(FormEvents::SUBMIT, function(FormEvent $event) use ($this) {
$entity = $event->getData();
$phoneNumber = $this->phoneNumberUtil->parse($entity->getOriginal(), PhoneNumberUtil::UNKNOWN_REGION);
$normalized_phonenumber = $this->phoneNumberUtil->format($phoneNumberObj, PhoneNumberFormat::E164);
$entity->setPhoneNumber($normalized_phonenumber);
;
}
/**
* @param OptionsResolver $resolver
*/
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'data_class' => 'AppBundle'Entity'Phonenumber'
));
}
}
还有一件事,将您的表单类型声明为服务将使构建更简单,请看那里:http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services
编辑,这里是 YML 服务定义:
app.contact_type:
class: AppBundle'Form'PhonenumberType
arguments:
- @libphonenumber.phone_number_util
tags:
- { name: form.type, alias: 'phone_number' }