
How to add new fields to the customer address in Prestashop
In today’s tutorial, we will see how to add new fields to the customer address in Prestashop.
UPDATE! If you prefer a simple solution, I created a module that automates the process and does much more, find it here
Download Project Files- Version used: Prestashop 1.6 (compatible with Prestashop 1.5)
Plan of action
In order to add a new field to the address registration form, we will be adding a couple of overrides, as well as creating a new database field and modifying the address template file. If you are not familiar with Prestashop overrides, check out my article on how to extend Prestashop objects (although we will have to use a slightly different technique).
The address class and database table
First things first, we need something to play with. We want the customer to be able to fill in a field which doesn’t currently exist, therefore let’s login to the database, access the ps_address table and create a new field: my_custom_field. I made it a 64 long VARCHAR field.
Next we need to tell the address object (class) that it has a new field to deal with. Create a new file inside override/classes/ and call it Address.php. Add the following inside php tags:
class Address extends AddressCore { public $my_custom_field; public static $definition = array( 'table' => 'address', 'primary' => 'id_address', 'fields' => array( 'id_customer' => array('type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false), 'id_manufacturer' => array('type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false), 'id_supplier' => array('type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false), 'id_warehouse' => array('type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId', 'copy_post' => false), 'id_country' => array('type' => self::TYPE_INT, 'validate' => 'isUnsignedId', 'required' => true), 'id_state' => array('type' => self::TYPE_INT, 'validate' => 'isNullOrUnsignedId'), 'alias' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'required' => true, 'size' => 32), 'company' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 64), 'lastname' => array('type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 32), 'firstname' => array('type' => self::TYPE_STRING, 'validate' => 'isName', 'required' => true, 'size' => 32), 'vat_number' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName'), 'address1' => array('type' => self::TYPE_STRING, 'validate' => 'isAddress', 'required' => true, 'size' => 128), 'address2' => array('type' => self::TYPE_STRING, 'validate' => 'isAddress', 'size' => 128), 'postcode' => array('type' => self::TYPE_STRING, 'validate' => 'isPostCode', 'size' => 12), 'city' => array('type' => self::TYPE_STRING, 'validate' => 'isCityName', 'required' => true, 'size' => 64), 'other' => array('type' => self::TYPE_STRING, 'validate' => 'isMessage', 'size' => 300), 'phone' => array('type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 32), 'phone_mobile' => array('type' => self::TYPE_STRING, 'validate' => 'isPhoneNumber', 'size' => 32), 'dni' => array('type' => self::TYPE_STRING, 'validate' => 'isDniLite', 'size' => 16), 'deleted' => array('type' => self::TYPE_BOOL, 'validate' => 'isBool', 'copy_post' => false), 'date_add' => array('type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'copy_post' => false), 'date_upd' => array('type' => self::TYPE_DATE, 'validate' => 'isDateFormat', 'copy_post' => false), 'my_custom_field' => array('type' => self::TYPE_STRING, 'validate' => 'isGenericName', 'size' => 64), ), ); }
Explanation: if you read my article on how to extend Prestashop Objects, you will notice I approached this override a little differently. I am not assigning the new field upon instance creation, but I am directly overriding its definition. The reason is simple: the Address definition is called statically when creating a new address in the front office, and if we added the new field in the construct method, we would have missed it from the list.
Please notice that this definition comes from Prestashop 1.6.0.5, and I therefore recommend to use your own from the original Address class, in case you have a different Prestashop version.
The back office Address Controller
We are half the way through already, and at this point we need to override the back office controller responsible for displaying the address modification form for each customer.
Create a new file named AdminAddressesController.php in override/controllers/admin/. We need to extend the renderForm() method; thus, head over the main controllers/admin folder, open up the original AdminAddressesController.php, locate the renderForm() method and copy it. Then, add the following inside php tags in our new controller override:
Class AdminAddressesController extends AdminAddressesControllerCore { }
And paste the renderForm() code inside the class. Here is how mine looks like, taken from Prestashop 1.6.0.5:
Class AdminAddressesController extends AdminAddressesControllerCore { public function renderForm() { $this->fields_form = array( 'legend' => array( 'title' => $this->l('Addresses'), 'icon' => 'icon-envelope-alt' ), 'input' => array( array( 'type' => 'text_customer', 'label' => $this->l('Customer'), 'name' => 'id_customer', 'required' => false, ), array( 'type' => 'text', 'label' => $this->l('Identification Number'), 'name' => 'dni', 'required' => false, 'col' => '4', 'hint' => $this->l('DNI / NIF / NIE') ), array( 'type' => 'text', 'label' => $this->l('Address alias'), 'name' => 'alias', 'required' => true, 'col' => '4', 'hint' => $this->l('Invalid characters:').' <>;=#{}' ), array( 'type' => 'text', 'label' => $this->l('Home phone'), 'name' => 'phone', 'required' => false, 'col' => '4', 'hint' => Configuration::get('PS_ONE_PHONE_AT_LEAST') ? sprintf($this->l('You must register at least one phone number.')) : '' ), array( 'type' => 'text', 'label' => $this->l('Mobile phone'), 'name' => 'phone_mobile', 'required' => false, 'col' => '4', 'hint' => Configuration::get('PS_ONE_PHONE_AT_LEAST') ? sprintf($this->l('You must register at least one phone number.')) : '' ), array( 'type' => 'textarea', 'label' => $this->l('Other'), 'name' => 'other', 'required' => false, 'cols' => 15, 'rows' => 3, 'hint' => $this->l('Forbidden characters:').' <>;=#{}' ), ), 'submit' => array( 'title' => $this->l('Save'), ) ); $id_customer = (int)Tools::getValue('id_customer'); if (!$id_customer && Validate::isLoadedObject($this->object)) $id_customer = $this->object->id_customer; if ($id_customer) { $customer = new Customer((int)$id_customer); $token_customer = Tools::getAdminToken('AdminCustomers'.(int)(Tab::getIdFromClassName('AdminCustomers')).(int)$this->context->employee->id); } $this->tpl_form_vars = array( 'customer' => isset($customer) ? $customer : null, 'tokenCustomer' => isset ($token_customer) ? $token_customer : null ); // Order address fields depending on country format $addresses_fields = $this->processAddressFormat(); // we use delivery address $addresses_fields = $addresses_fields['dlv_all_fields']; $temp_fields = array(); foreach ($addresses_fields as $addr_field_item) { if ($addr_field_item == 'company') { $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('Company'), 'name' => 'company', 'required' => false, 'col' => '4', 'hint' => $this->l('Invalid characters:').' <>;=#{}' ); $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('VAT number'), 'col' => '2', 'name' => 'vat_number' ); } else if ($addr_field_item == 'lastname') { if (isset($customer) && !Tools::isSubmit('submit'.strtoupper($this->table)) && Validate::isLoadedObject($customer) && !Validate::isLoadedObject($this->object)) $default_value = $customer->lastname; else $default_value = ''; $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('Last Name'), 'name' => 'lastname', 'required' => true, 'col' => '4', 'hint' => $this->l('Invalid characters:').' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', 'default_value' => $default_value, ); } else if ($addr_field_item == 'firstname') { if (isset($customer) && !Tools::isSubmit('submit'.strtoupper($this->table)) && Validate::isLoadedObject($customer) && !Validate::isLoadedObject($this->object)) $default_value = $customer->firstname; else $default_value = ''; $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('First Name'), 'name' => 'firstname', 'required' => true, 'col' => '4', 'hint' => $this->l('Invalid characters:').' 0-9!&lt;&gt;,;?=+()@#"�{}_$%:', 'default_value' => $default_value, ); } else if ($addr_field_item == 'address1') { $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('Address'), 'name' => 'address1', 'col' => '6', 'required' => true, ); } else if ($addr_field_item == 'address2') { $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('Address').' (2)', 'name' => 'address2', 'col' => '6', 'required' => false, ); } elseif ($addr_field_item == 'postcode') { $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('Zip/Postal Code'), 'name' => 'postcode', 'col' => '2', 'required' => true, ); } else if ($addr_field_item == 'city') { $temp_fields[] = array( 'type' => 'text', 'label' => $this->l('City'), 'name' => 'city', 'col' => '4', 'required' => true, ); } else if ($addr_field_item == 'country' || $addr_field_item == 'Country:name') { $temp_fields[] = array( 'type' => 'select', 'label' => $this->l('Country'), 'name' => 'id_country', 'required' => false, 'col' => '4', 'default_value' => (int)$this->context->country->id, 'options' => array( 'query' => Country::getCountries($this->context->language->id), 'id' => 'id_country', 'name' => 'name' ) ); $temp_fields[] = array( 'type' => 'select', 'label' => $this->l('State'), 'name' => 'id_state', 'required' => false, 'col' => '4', 'options' => array( 'query' => array(), 'id' => 'id_state', 'name' => 'name' ) ); } } // merge address format with the rest of the form array_splice($this->fields_form['input'], 3, 0, $temp_fields); return parent::renderForm(); } }
We must edit the $this->fields_form variable to add a new input field; right after this:
array( 'type' => 'text', 'label' => $this->l('Identification Number'), 'name' => 'dni', 'required' => false, 'col' => '4', 'hint' => $this->l('DNI / NIF / NIE') ),
Add the following
array( 'type' => 'text', 'label' => $this->l('My custom field'), 'name' => 'my_custom_field', 'required' => false, 'col' => '4', 'hint' => $this->l('Just a custom field!') ),
Note: make sure the “name” matches the one of the new attribute previously added to both the database and Address class object!
Lastly, at the end of the method, change:
return parent::renderForm();
To
return AdminController::renderForm();
So that the new fields list is not replaced by the original one.
If the Address class or/and controller were not previously overridden, at this point you must reach the cache/ folder and erase class_index.php; as always. After doing it, overrides will take place.
The Front Office Address Template
As very last step, let’s now add the custom field to the front office template, so it can be filled in by clients when they register. Open up address.tpl, located in the theme folder. I am using the default bootstrap template, so your file might look differently in case you have a custom one! Locate:
{if $field_name eq 'vat_number'} <div id="vat_area"> <div id="vat_number"> <div class="form-group"> <label for="vat-number">{l s='VAT number'}</label> <input type="text" class="form-control validate" data-validate="{$address_validation.$field_name.validate}" id="vat-number" name="vat_number" value="{if isset($smarty.post.vat_number)}{$smarty.post.vat_number}{else}{if isset($address->vat_number)}{$address->vat_number|escape:'html':'UTF-8'}{/if}{/if}" /> </div> </div> </div> {/if}
And right after it, add:
{if $field_name eq 'my_custom_field'} <div id="vat_area"> <div id="my_custom_field"> <div class="form-group"> <label for="my-custom-field">{l s='My Custom Field'}</label> <input type="text" class="form-control validate" data-validate="{$address_validation.$field_name.validate}" id="my-custom-field" name="my_custom_field" value="{if isset($smarty.post.my_custom_field)}{$smarty.post.my_custom_field}{else}{if isset($address->my_custom_field)}{$address->my_custom_field|escape:'html':'UTF-8'}{/if}{/if}" /> </div> </div> </div> {/if}
Save and refresh, we’re done!
TIP: if you don’t see any change in the front office, make sure you turn on recompilation from Advanced Parameters > Performance, and clear Smarty cache as well
Optional: Making the new field mandatory
If you really need your customers to input something in that new field, than another override is necessary. Create a new file within override/controllers/front and call it AddressController.php. Paste the following inside php tags, ad always:
Class AddressController extends AddressControllerCore { }
We need to override the processSubmitAddress() method. Therefore, if your Prestashop version is not 1.6.0.5, go ahead and copy your original one inside this new override; otherwise you can use the following:
protected function processSubmitAddress() { $address = new Address(); $this->errors = $address->validateController(); $address->id_customer = (int)$this->context->customer->id; // Check page token if ($this->context->customer->isLogged() && !$this->isTokenValid()) $this->errors[] = Tools::displayError('Invalid token.'); // Check phone if (Configuration::get('PS_ONE_PHONE_AT_LEAST') && !Tools::getValue('phone') && !Tools::getValue('phone_mobile')) $this->errors[] = Tools::displayError('You must register at least one phone number.'); if ($address->id_country) { // Check country if (!($country = new Country($address->id_country)) || !Validate::isLoadedObject($country)) throw new PrestaShopException('Country cannot be loaded with address->id_country'); if ((int)$country->contains_states && !(int)$address->id_state) $this->errors[] = Tools::displayError('This country requires you to chose a State.'); $postcode = Tools::getValue('postcode'); /* Check zip code format */ if ($country->zip_code_format && !$country->checkZipCode($postcode)) $this->errors[] = sprintf(Tools::displayError('The Zip/Postal code you\'ve entered is invalid. It must follow this format: %s'), str_replace('C', $country->iso_code, str_replace('N', '0', str_replace('L', 'A', $country->zip_code_format)))); elseif(empty($postcode) && $country->need_zip_code) $this->errors[] = Tools::displayError('A Zip/Postal code is required.'); elseif ($postcode && !Validate::isPostCode($postcode)) $this->errors[] = Tools::displayError('The Zip/Postal code is invalid.'); // Check country DNI if ($country->isNeedDni() && (!Tools::getValue('dni') || !Validate::isDniLite(Tools::getValue('dni')))) $this->errors[] = Tools::displayError('The identification number is incorrect or has already been used.'); else if (!$country->isNeedDni()) $address->dni = null; } // Check if the alias exists if (!$this->context->customer->is_guest && !empty($_POST['alias']) && (int)$this->context->customer->id > 0) { $id_address = Tools::getValue('id_address'); if(Configuration::get('PS_ORDER_PROCESS_TYPE') && (int)Tools::getValue('opc_id_address_'.Tools::getValue('type')) > 0) $id_address = Tools::getValue('opc_id_address_'.Tools::getValue('type')); if (Db::getInstance()->getValue(' SELECT count(*) FROM '._DB_PREFIX_.'address WHERE `alias` = \''.pSql($_POST['alias']).'\' AND id_address != '.(int)$id_address.' AND id_customer = '.(int)$this->context->customer->id.' AND deleted = 0') > 0) $this->errors[] = sprintf(Tools::displayError('The alias "%s" has already been used. Please select another one.'), Tools::safeOutput($_POST['alias'])); } // Check the requires fields which are settings in the BO $this->errors = array_merge($this->errors, $address->validateFieldsRequiredDatabase()); // Don't continue this process if we have errors ! if ($this->errors && !$this->ajax) return; // If we edit this address, delete old address and create a new one if (Validate::isLoadedObject($this->_address)) { if (Validate::isLoadedObject($country) && !$country->contains_states) $address->id_state = 0; $address_old = $this->_address; if (Customer::customerHasAddress($this->context->customer->id, (int)$address_old->id)) { if ($address_old->isUsed()) $address_old->delete(); else { $address->id = (int)($address_old->id); $address->date_add = $address_old->date_add; } } } if ($this->ajax && Tools::getValue('type') == 'invoice' && Configuration::get('PS_ORDER_PROCESS_TYPE')) { $this->errors = array_unique(array_merge($this->errors, $address->validateController())); if (count($this->errors)) { $return = array( 'hasError' => (bool)$this->errors, 'errors' => $this->errors ); die(Tools::jsonEncode($return)); } } // Save address if ($result = $address->save()) { // Update id address of the current cart if necessary if (isset($address_old) && $address_old->isUsed()) $this->context->cart->updateAddressId($address_old->id, $address->id); else // Update cart address $this->context->cart->autosetProductAddress(); if ((bool)(Tools::getValue('select_address', false)) == true OR (Tools::getValue('type') == 'invoice' && Configuration::get('PS_ORDER_PROCESS_TYPE'))) $this->context->cart->id_address_invoice = (int)$address->id; elseif (Configuration::get('PS_ORDER_PROCESS_TYPE')) $this->context->cart->id_address_invoice = (int)$this->context->cart->id_address_delivery; $this->context->cart->update(); if ($this->ajax) { $return = array( 'hasError' => (bool)$this->errors, 'errors' => $this->errors, 'id_address_delivery' => (int)$this->context->cart->id_address_delivery, 'id_address_invoice' => (int)$this->context->cart->id_address_invoice ); die(Tools::jsonEncode($return)); } // Redirect to old page or current page if ($back = Tools::getValue('back')) { if ($back == Tools::secureReferrer(Tools::getValue('back'))) Tools::redirect(html_entity_decode($back)); $mod = Tools::getValue('mod'); Tools::redirect('index.php?controller='.$back.($mod ? '&back='.$mod : '')); } else Tools::redirect('index.php?controller=addresses'); } $this->errors[] = Tools::displayError('An error occurred while updating your address.'); }
Right before the // CHeck Phone comment, add:
if ( !Tools::getValue('my_custom_field')) $this->errors[] = Tools::displayError('The custom field is mandatory!');
So that an error will be thrown if your clients leave that field empty!
Remember to erase the class index file again, of course.
Adding the new field to the address format in Prestashop
As a side note, be aware that in order to display the new field we added we need to change the address format for each country. I am not currently aware of a simple way to apply address changes to all countries at once. Therefore, head over to Localization > Countries, click on a country name and add the new field to the address box:
What for new Customer Fields?
Throughout this tutorial, we added a new field to the Address object. However, it is also possible to create a new basic customer field, to sit along name, lastname and email in the very basic registration (even without an address). To do so, you can simply apply the same process to the Customer class, and relative controllers. Just be aware that you will need to edit a couple of different template files: identity.tpl and authentication.tpl, as well as order-opc-new-account.tpl if you use the one page checkout!
UPDATE! If you prefer a simple solution, I created a module that automates the process and does much more, find it here