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

Popular posts from this blog

sequelize.js - Sequelize group by with association includes id -

android - Robolectric "INTERNET permission is required" -

java - Android raising EPERM (Operation not permitted) when attempting to send UDP packet after network connection -