Updating fields on a form with Ajax and Json

  1. Scenario
  2. Data structure
  3. Vehicle Controller
  4. Vehicle Form

Scenario

I'm developing a website that has an option to look up a car's registration number via SOAP and return additional details such as make, model, colour etc.

This should in turn update the fields for make, model, colour etc on the Vehicle form. It also checks to see that the details returned by SOAP are in the database and create them if not.

I'm by no means an expert in yii, I installed it a couple of weeks ago. This was a bit of a stumbling block for me and took me a few days to figure out. So thought I would share the code and hope it saves yii newbies a bit of time.

Acknowledgements

Zaccaria for pointing me in the right direction.

Data structure

CREATE TABLE IF NOT EXISTS `dms_vehicle` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `registration_number` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `chassis_number` varchar(255) COLLATE utf8_unicode_ci DEFAULT NULL,
  `vehicle_make_id` int(11) DEFAULT NULL,
  `vehicle_model_id` int(11) DEFAULT NULL,
  `vehicle_colour_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `FK_vehicle_vehicle_make` (`vehicle_make_id`),
  KEY `FK_vehicle_vehicle_model` (`vehicle_model_id`),
  KEY `FK_vehicle_vehicle_colour` (`vehicle_colour_id`),
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `dms_vehicle_colour` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `dvla_code` varchar(1) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  UNIQUE KEY `dvla_code` (`dvla_code`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `dms_vehicle_make` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `dvla_code` varchar(5) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  UNIQUE KEY `dvla_code` (`dvla_code`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=1 ;

CREATE TABLE IF NOT EXISTS `dms_vehicle_model` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `dvla_code` varchar(5) COLLATE utf8_unicode_ci NOT NULL,
  `name` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `vehicle_make_id` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `name` (`name`),
  UNIQUE KEY `dvla_code` (`dvla_code`),
  KEY `FK_vehicle_model_vehicle_make` (`vehicle_make_id`)
) ENGINE=InnoDB  DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci AUTO_INCREMENT=17 ;

ALTER TABLE `dms_vehicle`
  ADD CONSTRAINT `FK_vehicle_vehicle_colour` FOREIGN KEY (`vehicle_colour_id`) REFERENCES `dms_vehicle_colour` (`id`),
  ADD CONSTRAINT `FK_vehicle_vehicle_make` FOREIGN KEY (`vehicle_make_id`) REFERENCES `dms_vehicle_make` (`id`),
  ADD CONSTRAINT `FK_vehicle_vehicle_model` FOREIGN KEY (`vehicle_model_id`) REFERENCES `dms_vehicle_model` (`id`),

ALTER TABLE `dms_vehicle_model`
  ADD CONSTRAINT `FK_vehicle_model_vehicle_make` FOREIGN KEY (`vehicle_make_id`) REFERENCES `dms_vehicle_make` (`id`);

Vehicle Controller

Create the models and CRUD using gii, then edit VehicleController.php

Access rules

My action is called hpicheck, so add this to the access rules

public function accessRules() {
        return array(
            array('allow', // only registered users can view and update
                'actions' => array('index', 'view', 'admin','create', 'update', 'hpicheck' ),
                'users' => array('@'),
            ),
            array('allow', // Only admins can delete
                'actions' => array('delete'),
                'expression' => 'Yii::app()->getModule(\'user\')->isAdmin()',
            ),
            array('deny', // deny all users - default action
                'users' => array('*'),
            ),
        );
    }

SOAP/AJAX function

Then create the hpicheck function in the vehicle controller

public function actionHpiCheck() {
        if (Yii::app()->request->isAjaxRequest) {

            // get the parameter passed via ajax from the _form.php
            $registration_number = Yii::app()->request->getParam('registration_number');

            if ($registration_number == '')
            {
				// No registration number has been entered so send the error back to the form
                echo CJSON::encode(array(
                    'error' => 'true',
                    'status' => 'HPI check failed, please enter a registration number '
                ));
                // exit;
                Yii::app()->end();
			}
			else
            {
				// Create the options for SOAP
                $options = array(
                    'soap_version' => SOAP_1_2,
                    'exceptions' => true,
                    'trace' => 1,
                );

				// Create the parameters to be passed to the enquire() function
                $params->Request->Asset->Vrm = $registration_number;
                
                // url for the soap service
                $url = 'http://soapurl.com';

                try {
                
                    // Open a soap client
                    $soapClient = new SoapClient($url, $options);

					// call the soap service's function with the parameters
					// returns an object with additional vehicle details
					$hpi = $soapClient->enquire($params);
					
					// the enquire() function is provided by the vehicle checking company - it isn't a soap function.
					// To get a list of available functions, use var_dump($soapClient->__getFunctions());
                    
                    // Set the status to blank, this is for recording any errors
                    $status = '';
                    
                    // Get the make code and description from the $hpi object
					$makeCode = $hpi->RequestResults->Asset->PrimaryAssetData->DVLA->Make->Code;
					$makeDescription = $hpi->RequestResults->Asset->PrimaryAssetData->DVLA->Make->Description;
					
                    // Create blank variables just in case...
                    $makeOption = '';
                    $make_id=0;
                    
					// Have a look to see if the makeCode exists in the VehicleMake table
                    $make = VehicleMake::model()->find('dvla_code=:dvla_code', array(':dvla_code' => $makeCode));
                    
                    if ($make == null) {
                        // It doesn't exist so create it
                        try 
                        {
                            $make = new VehicleMake;
                            $make->dvla_code = $makeCode;
                            $make->name = $makeDescription;
                            $make->save();

                            $make_id = $make->id;
							$makeOption = CHtml::tag('option', array('value' => $make_id),
                                        CHtml::encode($makeDescription), true);

                        } 
                        catch (Exception $e) 
                        {
							// This will catch any database errors, duplicates for example
                            Yii::log($e->getMessage());
                            $status .= $e->getMessage() . '<br/>';
                            
                            // Send the error back to the form
                            echo CJSON::encode(array(
								'error' => 'true',
								'status' => $status,
							));

							// exit;
							Yii::app()->end();
                        }
                    } 
                    else 
                    {
						// We have found the make, so get the id
                        $make_id = $make->id;
                    }    
                    
                    // repeat the above code for VehicleModel and VehicleColour
                    // At some stage I'll create a generic function
					...
					
					
                    if ($status=='')
                    {
						// blank status so all is well
						$status = 'HPI check complete';
					}

					// Pass our data back to the _form.php via json
                    echo CJSON::encode(array(
                        'error' => 'false',
                        'status' => $status,
                        'colour_id' => $colour_id,
                        'colourOption' => $colourOption,
                        'make_id' => $make_id,
                        'makeOption' => $makeOption,
                        'model_id' => $model_id,
                        'modelOption' => $modelOption,
                    ));
                    
                    // exit;
                    Yii::app()->end();
                } 
                catch (SoapFault $soapFault) 
                {
					// Theres a problem with SOAP 
					// or the enquire() function threw an error, an invalid registration number for example
                    $status = 'HPI Error : ' . substr($soapFault->faultcode, strlen('soapenv:'));
                    $status .= ' (' . $soapFault->detail->HpiSoapFault->Error->Code . ') ';
                    $status .= $soapFault->detail->HpiSoapFault->Error->Description . '<br/>';

                    echo CJSON::encode(array(
                        'error' => 'true',
                        'status' => $status,
                    ));

                    // exit;
                    Yii::app()->end();
                }
            }

        }
    }

Vehicle Form

Finally we need modify the vehicle view _form.php to send and receive the data

<div class="row">
		<?php echo $form->labelEx($model,'registration_number'); ?>
		<?php echo $form->textField($model,'registration_number',array('size'=>8,'maxlength'=>8)); ?>
		<?php echo $form->error($model,'registration_number'); ?>
	</div>
	
	<?php echo CHtml::button('HPI Check', array( 'onclick'=>"{hpiCheck();}" ) ); ?>

	<div id='hpistatus'></div>
	
	<div class="row">
		<?php echo $form->labelEx($model,'vehicle_make_id'); ?>
		<?php echo $form->dropDownList($model,'vehicle_make_id', CHtml::listData(VehicleMake::model()->findAll(), 'id', 'name')); ?>
		<?php echo $form->error($model,'vehicle_make_id'); ?>
	</div>
	
	<div class="row">
		<?php echo $form->labelEx($model,'vehicle_model_id'); ?>
		<?php echo $form->dropDownList($model,'vehicle_model_id', CHtml::listData(VehicleModel::model()->findAll(), 'id', 'name')); ?>
		<?php echo $form->error($model,'vehicle_model_id'); ?>
	</div>
	
	<div class="row">
		<?php echo $form->labelEx($model,'vehicle_colour_id'); ?>
		<?php echo $form->dropDownList($model,'vehicle_colour_id', CHtml::listData(VehicleColour::model()->findAll(), 'id', 'name')); ?>
		<?php echo $form->error($model,'vehicle_colour_id'); ?>
	</div>
	 
	<script type="text/javascript">
	// The bits above should be familiar to you
	// This function is called by the "HPI check" button above
	function hpiCheck()
	{
		<?php echo CHtml::ajax(array(
				// the controller/function to call
				'url'=>CController::createUrl('vehicle/hpiCheck'), 
				
				// Data to be passed to the ajax function
				// Note that the ' should be escaped with \
				// The field id should be prefixed with the model name eg Vehicle_field_name
				'data'=>array('registration_number'=>'js:$(\'#Vehicle_registration_number\').val()', 
				
				// To pass multiple fields, just repeat eg:
				//				'chassis_number'=>'js:$(\'#Vehicle_chassis_number\').val()',
								),
				'type'=>'post',
				'dataType'=>'json',
				'success'=>"function(data)
				{
					// data will contain the json data passed by the hpicheck action in the controller
					// Update the status
					$('#hpistatus').html(data.status);
					if (data.error=='false')
					{
						// If there are no errors then update the fields
						// As mentioned above, the id is prefixed by the model name eg Vehicle_field_name
						
						// If its a new record, then add it to the select options first
						if (data.colourOption!='') $('#Vehicle_vehicle_colour_id').append(data.colourOption);
						// Then change the selected option
						$('#Vehicle_vehicle_colour_id').val(data.colour_id);
						if (data.makeOption!='') $('#Vehicle_vehicle_make_id').append(data.makeOption);
						$('#Vehicle_vehicle_make_id').val(data.make_id);
						if (data.modelOption!='') $('#Vehicle_vehicle_model_id').append(data.modelOption);
						$('#Vehicle_vehicle_model_id').val(data.model_id);
					}

				} ",
				))?>;
		return false;  
	} 
	</script>