Search code examples
yiiyii2yii2-advanced-appyii-extensions

Yii2: show user friendly validation errors when using try catch with transactions


I am using bootstrap ActiveForm. Here is my form:

use yii\helpers\Html;
use yii\bootstrap\ActiveForm;
use kartik\file\FileInput;

/* @var $this yii\web\View */
/* @var $model common\models\Customer */
/* @var $form yii\widgets\ActiveForm */
?>
<?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data'],
    'id' => 'customer-form',
    'enableClientValidation' => true,
    'options' => [
        'validateOnSubmit' => true,
        'class' => 'form'
    ],
    'layout' => 'horizontal',
    'fieldConfig' => [
      'horizontalCssClasses' => [
          'label' => 'col-sm-4',
         // 'offset' => 'col-sm-offset-2',
          'wrapper' => 'col-sm-8',
      ],
    ],
]); ?>
<?= $form->field($model, 'email')->textInput(['maxlength' => true]) ?>
<?php ActiveForm::end(); ?>

Here is my model:

class Customer extends \yii\db\ActiveRecord
{
    public $username;
    public $password;
    public $status;
    public $email;
    public $uploads;


    public function rules()
    {
        return [
            [['user_id', 'created_by', 'updated_by'], 'integer'],
            [['created_at','uploads', 'updated_at','legacy_customer_id','fax','phone_two','trn'], 'safe'],
            [['company_name','customer_name','username','password', 'tax_id'], 'string', 'max' => 200],
            [['customer_name','email','legacy_customer_id','company_name','city'], 'required'],
            [['is_deleted','status'], 'boolean'],
            [['address_line_1', 'state','phone', 'country'], 'string', 'max' => 450],
            [['address_line_2', 'city', 'zip_code'], 'string', 'max' => 45],
            [['user_id','legacy_customer_id'], 'unique'],
            ['email', 'email'],
            [['uploads'], 'file',  'maxFiles' => 10],
            [['email'], 'unique', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['email' => 'email'], 'message' => 'This email address has already been taken.'],
            [['user_id'], 'exist', 'skipOnError' => true, 'targetClass' => User::className(), 'targetAttribute' => ['user_id' => 'id']],
        ];
    }
}

Here is Controller

 public function actionCreate() {
        $model = new Customer();
        if ($model->load(Yii::$app->request->post())) {
            $transaction = Yii::$app->db->beginTransaction();
            try 
            {
            $user_create = \common\models\User::customeruser($model);
            if ($user_create) {
                $model->user_id = $user_create->id;
                $auth = \Yii::$app->authManager;
                $role = $auth->getRole('customer');
                $auth->assign($role, $model->user_id);
            }
            if ($user_create && $model->save()) {

                $photo = UploadedFile::getInstances($model, 'uploads');
                if ($photo !== null) {
                    $save_images = \common\models\CustomerDocuments::save_document($model->user_id, $photo);
                }
                $transaction->commit();  
                return $this->redirect(['view', 'id' => $model->user_id]);
            }
        }catch (Exception $e) 
        {
          $transaction->rollBack();
        }
    }

        if (Yii::$app->request->isAjax) {
            return $this->renderAjax('create', [
                        'model' => $model,
            ]);
        } else {
            return $this->render('create', [
                        'model' => $model,
            ]);
        }
    }

Now the required attribute working used in the rules. It does not allow the form to submit until the required field filled with some value, but at the same time the unique attribute using with target class not working and allow the form to submit. After click the submit form the form will not submit but it does not show the error of unique validation. Regard I am using the form in bootstrap modal and I want the form will show the unique submit error before submission like the same as required working. I can do it using jQuery on blur function and send custom AJAX request but I want the default solution of Yii 2.

EDIT

This is where the error is throw due to user not being saved

public static function customeruser( $model ) {
    $user = new User();
    $user->username = $model->email;
    $user->email = $model->email;
    $user->setPassword ( $model->legacy_customer_id );
    $user->generateAuthKey ();
    if ( !$user->save () ) {
        var_dump ( $user->getErrors () );
        exit ();
    } return $user->save () ? $user : null;
}

var_dump() shows the following

'username' => array (size = 1) 0 => string 'This username has already been taken.' (length = 37) 'email' => array (size = 1) 0 => string 'This email address has already been taken.' (length = 42).


Solution

  • As you are using the try catch block along with the transaction you should throw and catch such errors as the exception so that the transaction is rolled back, and the message is displayed to the user too.

    You are not consuming or using the beauty of try{}catch(){} block with transactions. You should always throw an Exception in case any of the models are not saved and the catch block will rollback the transaction.

    For example, you are saving the user in the function customeruser() by calling

    $user_create = \common\models\User::customeruser($model);
    

    and returning the user object or null otherwise and then in the very next line, you are verifying the user was created or not.

    if ($user_create) {
    

    You should simply throw an exception from the function customeruser() in case the model was not saved and return the $user object otherwise, you don't have to check $user_create again to verify if user was not saved the exception will be thrown and control will be transferred to the catch block and the lines after $user_create = \common\models\User::customeruser($model); will never be called.

    I mostly use the following way when i have multiple models to save and i am using the transaction block.

    $transaction = Yii::$app->db->beginTransaction ();
    try {
        if ( !$modelUser->save () ) {
            throw new \Exception ( implode ( "<br />" , \yii\helpers\ArrayHelper::getColumn ( $modelUser->errors , 0 , false ) ) );
        }
        if ( !$modelProfile->save () ) {
            throw new \Exception ( implode ( "<br />" , \yii\helpers\ArrayHelper::getColumn ( $modelProfile->errors , 0 , false ) ) );
        }
        $transaction->commit();
    } catch ( \Exception $ex ) {
        $transaction->rollBack();
        Yii::$app->session->setFlash ( 'error' , $ex->getMessage () );
    }
    

    So you can do the same for your code

    public static function customeruser( $model ) {
        $user = new User();
        $user->username = $model->email;
        $user->email = $model->email;
        $user->setPassword ( $model->legacy_customer_id );
        $user->generateAuthKey ();
        if(!$user->save()){
            throw new \Exception ( implode ( "<br />" , \yii\helpers\ArrayHelper::getColumn ( $user->errors , 0 , false ) ) );
        }
        return $user;
    }
    

    change your actionCreate to the following

    public function actionCreate() {
        $model = new Customer();
        if ( $model->load ( Yii::$app->request->post () ) ) {
            $transaction = Yii::$app->db->beginTransaction ();
            try {
                $user_create = \common\models\User::customeruser ( $model );
                $model->user_id = $user_create->id;
                $auth = \Yii::$app->authManager;
                $role = $auth->getRole ( 'customer' );
                $auth->assign ( $role , $model->user_id );
    
                if ( !$model->save () ) {
                    throw new \Exception ( implode ( "<br />" , \yii\helpers\ArrayHelper::getColumn ( $model->errors , 0 , false ) ) );
                }
    
                $photo = UploadedFile::getInstances ( $model , 'uploads' );
                if ( $photo !== null ) {
                    $save_images = \common\models\CustomerDocuments::save_document ( $model->user_id , $photo );
                }
    
                $transaction->commit ();
                return $this->redirect ( [ 'view' , 'id' => $model->user_id ] );
            } catch ( \Exception $ex ) {
                Yii::$app->session->setFlash ( 'error' , $ex->getMessage () );
                $transaction->rollBack ();
            }
        }
    
        if ( Yii::$app->request->isAjax ) {
            return $this->renderAjax ( 'create' , [
                        'model' => $model ,
                    ] );
        }
    
        return $this->render ( 'create' , [
                    'model' => $model ,
                ] );
    }