In this tutorial we will see step by step how to add the city field as a drop-down menu when creating or modifying an address.
The first step is to create a table with the cities. We will call the table city, and it will have this structure
The id_states must be given by each state that has your prestashop store. The table would look like this:
The next step is to modify the ps_address table by adding an id_city field.
Finally, we will modify the address_format table defining for which countries we will use the droptown format in the city.
Just edit the format field, which generally when it contains states looks like this:
And we will change the field city to city: name. Name would add the dropdown:
At this point the store will give us errors since classes are missing, so now we must modify the files so that all this works.
First we will create a City.php file in the classes folder that will allow us to work with cities. The file will contain:
class CityCore extends ObjectModel
{
public $id_city;
public $id_state;
public $name;
protected $fieldsRequired = array('id_city', 'name');
protected $fieldsSize = array('name' => 128, 'id_state' => 10);
protected $fieldsValidate = array('name' => 'isGenericName', 'id_state' => 'isUnsignedId', );
protected $table = 'city';
protected $identifier = 'id_city';
private static $_cache_get_cities = array();
public function getFields()
{
parent::validateFields();
$fields['id_city'] = (int)($this->id_city);
$fields['name'] = pSQL($this->name);
$fields['id_state'] = pSQL($this->id_state);
return $fields;
}
public function delete()
{
$id = $this->id;
parent::delete();
}
public static function getCities($id_state)
{
$id_city = Db::getInstance()->ExecuteS('
SELECT * FROM `'._DB_PREFIX_.'city`
WHERE `id_state` = '.(int)$id_state.'
ORDER BY `name`;'
);
return $id_city;
}
public static function getCityName($id_city)
{
return Db::getInstance()->getValue('
SELECT `name` FROM `'._DB_PREFIX_.'city`
WHERE `id_city` = '.(int)$id_city
);
}
}
Then we replace this function in classes/form/customeraddressform.php :
public function submit()
{
if (!$this->validate()) {
return false;
}
$address = new Address(
$this->getValue('id_address'),
$this->language->id
);
foreach ($this->formFields as $formField) {
if ($formField->getName() == 'id_city'){
$id_city = Db::getInstance()->getRow('
SELECT * FROM `'._DB_PREFIX_.'city`
WHERE `id_city` = '.$formField->getValue().';');
$address->city = $id_city['name'];
$address->{$formField->getName()} = $formField->getValue();
} else {
$address->{$formField->getName()} = $formField->getValue();
}
}
if (!isset($this->formFields['id_state'])) {
$address->id_state = 0;
}
if (empty($address->alias)) {
$address->alias = $this->translator->trans('My Address', [], 'Shop.Theme.Checkout');
}
Hook::exec('actionSubmitCustomerAddressForm', array('address' => &$address));
$this->setAddress($address);
$this->getPersister()->save(
$address,
$this->getValue('token'));
Db::getInstance()->Execute('UPDATE `'._DB_PREFIX_.'address` SET `city` = \''.$address->city.'\' WHERE id_address = '.$address->id);
return true;
}
The following file is the classes / form / CustomerAddressFormatter.php
We will look for this line of states
elseif ($entity === 'State') {
if ($this->country->contains_states) {
$states = State::getStatesByIdCountry($this->country->id, true);
foreach ($states as $state) {
$formField->addAvailableValue(
$state['id_state'],
$state[$entityField]
);
}
$formField->setRequired(true);
}
}
and we add this to the end of these lines
elseif ($entity === 'city') {
$formField->setType('select');
$formField->setName('id_' . strtolower($entity));
$cities = State::getCities(315);
foreach ($cities as $city) {
$formField->addAvailableValue(
$city['id_city'],
$city[$entityField]
);
}
$formField->setRequired(false);
}
And we add this function to the clases/State.php file
public static function getCities($id_state)
{
$id_city = Db::getInstance()->ExecuteS('
SELECT * FROM `'._DB_PREFIX_.'city`
WHERE `id_state` = '.(int)$id_state.'
ORDER BY `name`;'
);
return $id_city;
}
For these already created files we can always use an override to avoid losing changes when updating PrestaShop
The next file is the classes / Address.php
We will add public variables at the beginning:
public $id_city;
public $cityName;
in the function public static $definition = array(
We will add at the end the created field id_city for the address table
'id_city' => 'isUnsignedId',
And we add this new function
public function getFields()
{
if (isset($this->id))
$sql = 'select * from '. _DB_PREFIX_ . 'city
WHERE id_city= ' . $this->id_city;
$sql2 = Db::getInstance()->getRow($sql);
$fields['id_address'] = (int)($this->id);
$fields['id_customer'] = is_null($this->id_customer) ? 0 : (int)($this->id_customer);
$fields['id_manufacturer'] = is_null($this->id_manufacturer) ? 0 : (int)($this->id_manufacturer);
$fields['id_supplier'] = is_null($this->id_supplier) ? 0 : (int)($this->id_supplier);
$fields['id_country'] = (int)($this->id_country);
$fields['id_state'] = (int)($this->id_state);
$fields['alias'] = pSQL($this->alias);
$fields['company'] = pSQL($this->company);
$fields['lastname'] = pSQL($this->lastname);
$fields['firstname'] = pSQL($this->firstname);
$fields['address1'] = pSQL($this->address1);
$fields['address2'] = pSQL($this->address2);
$fields['postcode'] = pSQL($this->postcode);
$fields['city'] = pSQL($sql2['name']);
$fields['other'] = pSQL($this->other);
$fields['phone'] = pSQL($this->phone);
$fields['phone_mobile'] = pSQL($this->phone_mobile);
$fields['vat_number'] = pSQL($this->vat_number);
$fields['dni'] = pSQL($this->dni);
$fields['deleted'] = (int)($this->deleted);
$fields['date_add'] = pSQL($this->date_add);
$fields['date_upd'] = pSQL($this->date_upd);
$fields['id_city'] = is_null($this->id_city) ? 0 : (int)($this->id_city);
return $fields;
}
And change this function at the end
public static function initialize($id_address = null, $with_geoloc = false)
where this elseif we add the city field
} elseif ($with_geoloc && isset($context->customer->geoloc_id_country)) {
$address = new Address();
$address->id_country = (int) $context->customer->geoloc_id_country;
$address->id_state = (int) $context->customer->id_state;
$address->id_city = (int) $context->customer->id_city;
$address->postcode = $context->customer->postcode;
}
The next step is to add the javascript code so that it takes the cities when changing state:
For this we will modify the file themes / ourtheme / templates / _partials / javascript.tpl of our theme and we will add this code at the end:
<!-- direcciones -->
{literal}
<script>
(function(){"use strict";var c=[],f={},a,e,d,b;if(!window.jQuery){a=function(g){c.push(g)};f.ready=function(g){a(g)};e=window.jQuery=window.$=function(g){if(typeof g=="function"){a(g)}return f};window.checkJQ=function(){if(!d()){b=setTimeout(checkJQ,100)}};b=setTimeout(checkJQ,100);d=function(){if(window.jQuery!==e){clearTimeout(b);var g=c.shift();while(g){jQuery(g);g=c.shift()}b=f=a=e=d=window.checkJQ=null;return true}return false}}})();
</script>
{/literal}
{if $page.page_name == "address" or $page.page_name == "order" or $page.page_name == "checkout"}
<script type="text/javascript">
$(document).ready(function(){
$(".form-control-select .js-city").last().val();
var mi_ajaxurl = '{$urls.base_url}modules/';
var aux_id_state = {if isset($smarty.post.id_state) and $smarty.post.id_state <> null}{$smarty.post.id_state}{else}{if isset($customer.addresses[$smarty.get.id_address].id_state)}{$customer.addresses[$smarty.get.id_address].id_state}{else}0{/if}{/if};
var aux_id_city = {if isset($smarty.post.id_city) and $smarty.post.id_city <> null}{$smarty.post.id_city}{else}{if isset($customer.addresses[$smarty.get.id_address].id_city)}{$customer.addresses[$smarty.get.id_address].id_city}{else}0{/if}{/if};
var aux_city = '{if isset($smarty.post.city) and $smarty.post.city <> null}{$smarty.post.city}{else}{if isset($customer.addresses[$smarty.get.id_address].city)}{$customer.addresses[$smarty.get.id_address].city}{else}0{/if}{/if}';
{literal}
$(document).ready(function(){
$.urlParam = function(name){
var results = new RegExp('[\?&]' + name + '=([^&#]*)').exec(window.location.href);
if (results==null) {
return null;
}
return decodeURI(results[1]) || 0;
}
ajaxCity();
$("[name='id_state']").change(function() {
ajaxCity();
});
$("[name='id_city']").change(function() {
});
function ajaxCity(valueaaa){
$.ajax({
type: "GET",
url: mi_ajaxurl+"ajax_addresses/ajax.php?ajaxCity=1&id_state="+$("[name='id_state']").val()+"&aux_id_state="+aux_id_state+"&aux_city="+aux_city,
success: function(r){
if( r == 'false' ){
$("[name='id_city']").fadeOut();
$("[name='id_city'] option[value=0]").attr("selected", "selected");
}else{
$("[name='id_city']").html(r);
$("[name='id_city']").fadeIn();
//$('#id_city option[value=0]').attr("selected", "selected");
$("[name='id_city'] option[value='+aux_id_city+']").attr("selected", "selected");
}
$("[name='id_city']").trigger('click');
//$("#id_street").trigger('click');
/*/***ajaxStreet();*/
}
});
};
});
{/literal}
</script>
{/if}
Finally, we will create a pseudo module for Ajax functions. This can be done in other ways, this is just a simple one: In the modules folder, we create a folder called ajax_addresses and within it a file called ajax.php with this content:
<?php
include(dirname(__FILE__). '/../../config/config.inc.php');
include(dirname(__FILE__). '/../../init.php');
// obtengo city
if (isset($_GET['ajaxCity']) AND isset($_GET['id_state']))
{
$idTemp = ( (int)(Tools::getValue('id_state')) == 0 ? (int)(Tools::getValue('aux_id_state')) : (int)(Tools::getValue('id_state')) );
$idcity = Tools::getValue('aux_city');
$states = Db::getInstance()->ExecuteS('
SELECT C.id_city, C.name
FROM '._DB_PREFIX_.'city C
WHERE C.id_state = '.$idTemp.'
ORDER BY C.`name` ASC');
$states2 = Db::getInstance()->getRow('
SELECT C.id_city, C.name
FROM '._DB_PREFIX_.'city C
WHERE C.name = \''.$idcity.'\'
ORDER BY C.`name` ASC');
//var_dump($states);
if (is_array($states) AND !empty($states))
{
$list = '';
if (Tools::getValue('aux_id_state') != true)
if($idcity != null){
$list = '<option value="'.$states2['name'].'" class="showme" >'.($idcity == 0 ? "---" : $idcity) .'</option>'."\n";
}
foreach ($states AS $state)
if($idcity == 0){
$list .= '<option value="'.(int)($state['id_city']).'"'.((isset($_GET['id_city']) AND $_GET['id_city'] == $state['id_city']) ? ' selected="selected"' : '').'>'.$state['name'].'</option>'."\n";
} else{
$list .= '<option value="'.(int)($state['id_city']).'"'.((isset($idcity) AND $idcity == $state['name']) ? ' selected="selected"' : '').'>'.$state['name'].'</option>'."\n";
}
}
else
$list = 'false';
die($list);
}
Luego solo debemos limpiar cache y ya deberíamos ver las ciudades como dropdown.
Link to files:
https://github.com/shacker2/citiesasdropdownprestashop/tree/main