php - Symfony2: Unique constraint for a string property is not working for values set in prePersist() method in a subscriber class -
i have form user enters phone number. common problem phone number can written in many different ways: "+49 711 xxxxxx", "0049 (0)711 xxxxxx" or "+49 711 - xxxxxx" presentations of same phone number. in order detect duplicates use "phone-number-bundle" (https://github.com/misd-service-development/phone-number-bundle) "normalized" e.164 representation of phone number can used comparison. if duplicate detected, entered number must not stored , notice has shown user.
if entered phone number valid phone number, want check if e.164-formatted value of phone number stored in database table.
this mysql table phone numbers:
-+----+---------------------+----------------+ | id | original | phonenumber | -+----+---------------------+----------------+ | 1 | 0711-xxxxxxx | +49711xxxxxxx | -+----+---------------------+----------------+ | 2 | +49 7034 / xxxxx-xx | +497034xxxxxxx | -+----+---------------------+----------------+ | 3 | +49 (0)171/xxxxxxx | +49171xxxxxxx | -+----+---------------------+----------------+ | .. | ... | ... | -+----+---------------------+----------------+
"phonenumber" contains e.164 formatted value of value entered in form. first entered value stored in column "original" additional information.
the form defined in "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 see unique constraint works when phonenumber submitted via form ->add('original') ; } /** * @param optionsresolver $resolver */ public function configureoptions(optionsresolver $resolver) { $resolver->setdefaults(array( 'data_class' => 'appbundle\entity\phonenumber' )); } }
phonenumber entity "src/appbundle/entity/phonenumber.php":
<?php namespace appbundle\entity; use doctrine\orm\mapping orm; use symfony\component\validator\constraints assert; use appbundle\validator\constraints 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(); } /** * 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; } /** * 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; } /** * original * * @return string */ public function getoriginal() { return $this->original; } }
defined services in "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 }
the subscriber class "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 stored first time public function prepersist(lifecycleeventargs $args) { $entity = $args->getentity(); // 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 stored public function preupdate(lifecycleeventargs $args) { $entity = $args->getentity(); // 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); } } }
the constraint class "src/appbundle/validator/constraints/isvalidphonenumber.php":
<?php namespace appbundle\validator\constraints; use symfony\component\validator\constraint; /** * @annotation */ class isvalidphonenumber extends constraint { public $message_invalid = 'not valid phone number: "%string%"'; /** * @return string */ public function validatedby() { return 'phonenumber_validation'; } }
the validator class "src/appbundle/validator/constraints/isvalidphonenumbervalidator.php":
<?php namespace appbundle\validator\constraints; use symfony\component\dependencyinjection\containerinterface 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(); } } } }
the phone number validator works - if number invalidated "phone-number-bundle", message "not valid phone number: "3333333333333333" displayed. e.164 formatted value gets saved correctly database table.
problem: although use "@orm\uniqueconstraint(columns={"phonenumber"})", "@uniqueentity("phonenumber")" , "unique=true" $phonenumber attribute in entity class, every entered valid number form gets stored in database, no matter if there duplicate in table or not. unique constraint not work when phonenumber field not added in form type class.
may interesting: when remove comment in phonenumbertype class that
->add('phonenumber')
is included again , existing number entered in associated form field "phonenumber", "this value used." expected.
what doing wrong?
thanks helping!
indeed, not. have differentiate:
- form validation. occurs when controller calls
$form->handle($request)
or similar, triggered on form event - doctrine callbacks, called during entitymanager
flush()
the solution use bundle not on prepersist()
, rather on form event. data normalized before writing entity, validating save db.
such code this:
<?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' )); } }
one more thing, declaring form type service make build simplfer, have there: http://symfony.com/doc/current/book/forms.html#defining-your-forms-as-services
edit, here yml service definition:
app.contact_type: class: appbundle\form\phonenumbertype arguments: - @libphonenumber.phone_number_util tags: - { name: form.type, alias: 'phone_number' }
Comments
Post a Comment