first commit
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/vendor
|
||||||
|
composer.lock
|
||||||
96
README.md
Normal file
96
README.md
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
# PrestaShop Phone Number Normalizer Module
|
||||||
|
|
||||||
|
[]
|
||||||
|
[](https://www.prestashop.com)
|
||||||
|
[](https://opensource.org/licenses/AFL-3.0)
|
||||||
|
|
||||||
|
Tired of inconsistent phone number formats in your customer database? `(555) 123-4567`, `555.123.4567`, `+1 555 123 4567`... This module solves the problem by automatically sanitizing and normalizing customer phone numbers to the international **E.164 standard** (e.g., `+15551234567`).
|
||||||
|
|
||||||
|
It uses the powerful `giggsey/libphonenumber-for-php` library, a PHP port of Google's `libphonenumber`, to intelligently parse and format phone numbers based on the customer's country.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Automatic Normalization on Save**: Hooks into the address creation and update process to normalize numbers in real-time. No manual action is needed for new addresses.
|
||||||
|
- **E.164 International Format**: Converts valid phone numbers into a consistent, machine-readable format perfect for SMS gateways and other integrations.
|
||||||
|
- **Country-Aware Parsing**: Uses the country selected in the customer's address as a hint to correctly interpret local and national phone number formats.
|
||||||
|
- **Safe Fallback**: If a number cannot be fully parsed into a valid international format, it saves a sanitized version (digits and `+` only), **ensuring no customer-provided digits are ever lost**.
|
||||||
|
- **Batch Processing**: Includes a Back Office tool to process and normalize all existing addresses in your database with a single click.
|
||||||
|
- **Detailed Logging**: Every change made to a phone number is recorded in a log file for auditing and debugging purposes.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
- PHP `8.1`
|
||||||
|
- PrestaShop `8.0`
|
||||||
|
- PrestaShop `9.x`
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You have two options for installation, depending on your needs.
|
||||||
|
|
||||||
|
### Method 1: Recommended (For Store Owners)
|
||||||
|
|
||||||
|
This is the easiest method. You will download a pre-packaged `.zip` file that already includes all the necessary libraries.
|
||||||
|
|
||||||
|
1. Go to the [Releases page](https://github.com/panariga/prestashop-phonenormalizer/releases) of this repository.
|
||||||
|
2. Download the latest `phonenormalizer.zip` file.
|
||||||
|
3. In your PrestaShop Back Office, navigate to **Modules > Module Manager**.
|
||||||
|
4. Click on the **"Upload a module"** button and select the `.zip` file you downloaded.
|
||||||
|
5. After the module uploads, find "Phone Number Normalizer" in the module list and click **Install**.
|
||||||
|
|
||||||
|
### Method 2: Manual / Developer (Using Composer)
|
||||||
|
|
||||||
|
Use this method if you have cloned the repository and have command-line access to your server.
|
||||||
|
|
||||||
|
1. Clone this repository into your PrestaShop `modules/` directory:
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/panariga/prestashop-phonenormalizer.git phonenormalizer
|
||||||
|
```
|
||||||
|
2. Navigate into the new module directory:
|
||||||
|
```bash
|
||||||
|
cd phonenormalizer
|
||||||
|
```
|
||||||
|
3. Install the required PHP dependencies using Composer:
|
||||||
|
```bash
|
||||||
|
composer install --no-dev --prefer-dist
|
||||||
|
```
|
||||||
|
4. In your PrestaShop Back Office, navigate to **Modules > Module Manager**.
|
||||||
|
5. Find "Phone Number Normalizer" in the module list and click **Install**.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Automatic Normalization
|
||||||
|
|
||||||
|
Once the module is installed, it works automatically. When a customer creates a new address or updates an existing one, the `phone` and `phone_mobile` fields will be processed and normalized before being saved to the database.
|
||||||
|
|
||||||
|
### Batch Normalization (For Existing Addresses)
|
||||||
|
|
||||||
|
To clean up all the addresses that were in your database before you installed the module:
|
||||||
|
|
||||||
|
1. Navigate to **Modules > Module Manager**.
|
||||||
|
2. Find **Phone Number Normalizer** in the list and click its **Configure** button.
|
||||||
|
3. You will see a "Batch Processing" panel. **Please read the warning!** It is highly recommended to **back up your `ps_address` database table** before running this process.
|
||||||
|
4. Click the **"Normalize All Existing Addresses"** button.
|
||||||
|
5. The process may take some time depending on the size of your database. Once complete, you will see a confirmation message indicating how many addresses were updated.
|
||||||
|
|
||||||
|
## Logging
|
||||||
|
|
||||||
|
All changes made by this module are logged for your review. This is useful for seeing exactly what was changed and why.
|
||||||
|
|
||||||
|
- **Log file location**: `[prestashop_root]/var/logs/modules/phonenormalizer/phonenormalizer.log`
|
||||||
|
|
||||||
|
- **Example log entry**:
|
||||||
|
```log
|
||||||
|
2023-10-27 14:35:01 [REAL-TIME] - Address ID: 12 - Changed field 'phone' FROM '(555) 123-4567' TO '+15551234567'
|
||||||
|
2023-10-27 14:40:11 [BATCH] - Address ID: 25 - Changed field 'phone_mobile' FROM '06.12.34.56.78' TO '+33612345678'
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This module is licensed under the [Academic Free License (AFL 3.0)](https://opensource.org/licenses/AFL-3.0).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Developed by [panariga](https://github.com/panariga).
|
||||||
16
composer.json
Normal file
16
composer.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{ "name": "panariga/phonenormalizer",
|
||||||
|
"description": "Sanitizes and normalizes customer phone numbers to E.164 format on address save.",
|
||||||
|
"type": "prestashop-module",
|
||||||
|
"require": {
|
||||||
|
"giggsey/libphonenumber-for-php": "^8.12",
|
||||||
|
"php": "^8.1"
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"prepend-autoloader": false
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"phonenormalizer\\": "src/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
303
phonenormalizer.php
Normal file
303
phonenormalizer.php
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 2025 Panariga
|
||||||
|
*
|
||||||
|
* NOTICE OF LICENSE
|
||||||
|
*
|
||||||
|
* This source file is subject to the Academic Free License (AFL 3.0)
|
||||||
|
* that is bundled with this package in the file LICENSE.txt.
|
||||||
|
* It is also available through the world-wide-web at this URL:
|
||||||
|
* http://opensource.org/licenses/afl-3.0.php
|
||||||
|
* If you did not receive a copy of the license and are unable to
|
||||||
|
* obtain it through the world-wide-web, please send an email
|
||||||
|
* to license@prestashop.com so we can send you a copy immediately.
|
||||||
|
*
|
||||||
|
* DISCLAIMER
|
||||||
|
*
|
||||||
|
* Do not edit or add to this file if you wish to upgrade PrestaShop to newer
|
||||||
|
* versions in the future. If you wish to customize PrestaShop for your
|
||||||
|
* needs please refer to http://www.prestashop.com for more information.
|
||||||
|
*
|
||||||
|
* @author Panariga
|
||||||
|
* @copyright 2025
|
||||||
|
* @license http://opensource.org/licenses/afl-3.0.php Academic Free License (AFL 3.0)
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (!defined('_PS_VERSION_')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require the Composer autoloader
|
||||||
|
if (file_exists(__DIR__ . '/vendor/autoload.php')) {
|
||||||
|
require_once __DIR__ . '/vendor/autoload.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
class PhoneNormalizer extends Module
|
||||||
|
{
|
||||||
|
protected $libraryLoaded = false;
|
||||||
|
protected $logFile;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->name = 'phonenormalizer';
|
||||||
|
$this->tab = 'administration';
|
||||||
|
$this->version = '1.0.1'; // Version bump for new feature
|
||||||
|
$this->author = 'Panariga';
|
||||||
|
$this->need_instance = 0;
|
||||||
|
$this->ps_versions_compliancy = ['min' => '8.2', 'max' => _PS_VERSION_];
|
||||||
|
$this->bootstrap = true;
|
||||||
|
|
||||||
|
parent::__construct();
|
||||||
|
|
||||||
|
$this->displayName = $this->l('Phone Number Normalizer');
|
||||||
|
$this->description = $this->l('Sanitizes and normalizes customer phone numbers to E.164 format on address save.');
|
||||||
|
$this->confirmUninstall = $this->l('Are you sure you want to uninstall?');
|
||||||
|
|
||||||
|
// Check if the main library class exists
|
||||||
|
if (class_exists('\\libphonenumber\\PhoneNumberUtil')) {
|
||||||
|
$this->libraryLoaded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Define the log file path
|
||||||
|
$this->logFile = _PS_ROOT_DIR_ . '/var/logs/modules/' . $this->name . '/' . $this->name . '.log';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function install()
|
||||||
|
{
|
||||||
|
if (!$this->libraryLoaded) {
|
||||||
|
$this->_errors[] = $this->l('The "giggsey/libphonenumber-for-php" library is not loaded. Please run "composer install" in the module directory.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the log directory on install
|
||||||
|
$logDir = dirname($this->logFile);
|
||||||
|
if (!is_dir($logDir)) {
|
||||||
|
mkdir($logDir, 0775, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register hooks to intercept address saving
|
||||||
|
return parent::install() &&
|
||||||
|
$this->registerHook('actionObjectAddressAddBefore') &&
|
||||||
|
$this->registerHook('actionObjectAddressUpdateBefore');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function uninstall()
|
||||||
|
{
|
||||||
|
return parent::uninstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook called before a new Address object is added to the database.
|
||||||
|
*/
|
||||||
|
public function hookActionObjectAddressAddBefore($params)
|
||||||
|
{
|
||||||
|
if (isset($params['object']) && $params['object'] instanceof Address) {
|
||||||
|
$this->processAddressNormalization($params['object']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook called before an existing Address object is updated in the database.
|
||||||
|
*/
|
||||||
|
public function hookActionObjectAddressUpdateBefore($params)
|
||||||
|
{
|
||||||
|
if (isset($params['object']) && $params['object'] instanceof Address) {
|
||||||
|
$this->processAddressNormalization($params['object']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Central logic to process both phone fields of an Address object.
|
||||||
|
*
|
||||||
|
* @param Address $address The address object, passed by reference.
|
||||||
|
* @param string $context The context of the action ('REAL-TIME' or 'BATCH').
|
||||||
|
*/
|
||||||
|
protected function processAddressNormalization(Address &$address, $context = 'REAL-TIME')
|
||||||
|
{
|
||||||
|
// Store original values for comparison
|
||||||
|
$originalPhone = $address->phone;
|
||||||
|
$originalMobile = $address->phone_mobile;
|
||||||
|
|
||||||
|
// Process 'phone' field
|
||||||
|
$newPhone = $this->normalizePhoneNumber($originalPhone, $address->id_country);
|
||||||
|
if ($newPhone !== $originalPhone) {
|
||||||
|
$address->phone = $newPhone;
|
||||||
|
$this->logChange($address->id, 'phone', $originalPhone, $newPhone, $context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process 'phone_mobile' field
|
||||||
|
$newMobile = $this->normalizePhoneNumber($originalMobile, $address->id_country);
|
||||||
|
if ($newMobile !== $originalMobile) {
|
||||||
|
$address->phone_mobile = $newMobile;
|
||||||
|
$this->logChange($address->id, 'phone_mobile', $originalMobile, $newMobile, $context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a single phone number string.
|
||||||
|
*
|
||||||
|
* @param string $phoneNumber The raw phone number string from user input.
|
||||||
|
* @param int $id_country The PrestaShop ID of the country for context.
|
||||||
|
* @return string The normalized phone number (E.164) or the sanitized version on failure.
|
||||||
|
*/
|
||||||
|
protected function normalizePhoneNumber($phoneNumber, $id_country)
|
||||||
|
{
|
||||||
|
if (!$this->libraryLoaded || empty($phoneNumber)) {
|
||||||
|
return $phoneNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. Sanitize the number: remove spaces and all non-numeric symbols except '+'
|
||||||
|
$sanitizedNumber = preg_replace('/[^\d+]/', '', (string)$phoneNumber);
|
||||||
|
|
||||||
|
$country = new Country($id_country);
|
||||||
|
$isoCode = $country->iso_code;
|
||||||
|
|
||||||
|
$phoneUtil = \libphonenumber\PhoneNumberUtil::getInstance();
|
||||||
|
try {
|
||||||
|
// 2. Try to parse the number using the country as a hint.
|
||||||
|
$numberProto = $phoneUtil->parse($sanitizedNumber, $isoCode);
|
||||||
|
|
||||||
|
// 3. Check if the parsed number is considered valid by the library.
|
||||||
|
if ($phoneUtil->isValidNumber($numberProto)) {
|
||||||
|
// 4. Format to E.164 standard
|
||||||
|
return $phoneUtil->format($numberProto, \libphonenumber\PhoneNumberFormat::E164);
|
||||||
|
}
|
||||||
|
} catch (\libphonenumber\NumberParseException $e) {
|
||||||
|
// Fall through to the return of the sanitized number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Fallback: return the sanitized number.
|
||||||
|
return $sanitizedNumber;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Writes a change to the log file.
|
||||||
|
*
|
||||||
|
* @param int $id_address
|
||||||
|
* @param string $fieldName 'phone' or 'phone_mobile'
|
||||||
|
* @param string $originalValue
|
||||||
|
* @param string $newValue
|
||||||
|
* @param string $context 'REAL-TIME' or 'BATCH'
|
||||||
|
*/
|
||||||
|
private function logChange($id_address, $fieldName, $originalValue, $newValue, $context)
|
||||||
|
{
|
||||||
|
$logMessage = sprintf(
|
||||||
|
"%s [%s] - Address ID: %d - Changed field '%s' FROM '%s' TO '%s'\n",
|
||||||
|
date('Y-m-d H:i:s'),
|
||||||
|
$context,
|
||||||
|
(int)$id_address,
|
||||||
|
$fieldName,
|
||||||
|
$originalValue,
|
||||||
|
$newValue
|
||||||
|
);
|
||||||
|
|
||||||
|
// Use FILE_APPEND to add to the log and LOCK_EX to prevent race conditions
|
||||||
|
file_put_contents($this->logFile, $logMessage, FILE_APPEND | LOCK_EX);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module configuration page content.
|
||||||
|
*/
|
||||||
|
public function getContent()
|
||||||
|
{
|
||||||
|
$output = '';
|
||||||
|
|
||||||
|
if (Tools::isSubmit('submitNormalizeAllAddresses')) {
|
||||||
|
$result = $this->runBatchNormalization();
|
||||||
|
if ($result['success']) {
|
||||||
|
$output .= $this->displayConfirmation(
|
||||||
|
sprintf($this->l('Successfully processed all addresses. %d addresses were updated. Check the log for details.'), $result['updated_count'])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
$output .= $this->displayError($this->l('An error occurred during batch processing.'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $output . $this->renderForm();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render the configuration form with the "Normalize All" button.
|
||||||
|
*/
|
||||||
|
public function renderForm()
|
||||||
|
{
|
||||||
|
if (!$this->libraryLoaded) {
|
||||||
|
return $this->displayError(
|
||||||
|
$this->l('The phone number library is missing. Please run "composer install" in the module\'s root directory (/modules/phonenormalizer/). The module functionality is currently disabled.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$helper = new HelperForm();
|
||||||
|
// ... (form configuration remains the same as before)
|
||||||
|
$helper->show_toolbar = false;
|
||||||
|
$helper->table = $this->table;
|
||||||
|
$helper->module = $this;
|
||||||
|
$helper->default_form_language = $this->context->language->id;
|
||||||
|
$helper->allow_employee_form_lang = Configuration::get('PS_BO_ALLOW_EMPLOYEE_FORM_LANG', 0);
|
||||||
|
$helper->identifier = $this->identifier;
|
||||||
|
$helper->submit_action = 'submitNormalizeAllAddresses';
|
||||||
|
$helper->currentIndex = $this->context->link->getAdminLink('AdminModules', false)
|
||||||
|
. '&configure=' . $this->name . '&tab_module=' . $this->tab . '&module_name=' . $this->name;
|
||||||
|
$helper->token = Tools::getAdminTokenLite('AdminModules');
|
||||||
|
|
||||||
|
$fields_form = [
|
||||||
|
'form' => [
|
||||||
|
'legend' => [
|
||||||
|
'title' => $this->l('Batch Processing'),
|
||||||
|
'icon' => 'icon-cogs',
|
||||||
|
],
|
||||||
|
'input' => [
|
||||||
|
[
|
||||||
|
'type' => 'html',
|
||||||
|
'name' => 'info_html',
|
||||||
|
'html_content' => '<p>' . $this->l('Click the button below to attempt to normalize all existing phone numbers in your customer addresses database.') . '</p>'
|
||||||
|
. '<p><strong>' . $this->l('Warning:') . '</strong> ' . $this->l('This action is irreversible and may take a long time on databases with many addresses. It is highly recommended to back up your `ps_address` table before proceeding.') . '</p>'
|
||||||
|
. '<p>' . sprintf($this->l('A log of all changes will be saved to: %s'), '<strong>' . str_replace(_PS_ROOT_DIR_, '', $this->logFile) . '</strong>') . '</p>',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'submit' => [
|
||||||
|
'title' => $this->l('Normalize All Existing Addresses'),
|
||||||
|
'class' => 'btn btn-default pull-right',
|
||||||
|
'icon' => 'process-icon-refresh',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $helper->generateForm([$fields_form]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the normalization process on all addresses in the database.
|
||||||
|
*/
|
||||||
|
protected function runBatchNormalization()
|
||||||
|
{
|
||||||
|
$updatedCount = 0;
|
||||||
|
$address_ids = Db::getInstance()->executeS('SELECT `id_address` FROM `' . _DB_PREFIX_ . 'address`');
|
||||||
|
|
||||||
|
if (empty($address_ids)) {
|
||||||
|
return ['success' => true, 'updated_count' => 0];
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($address_ids as $row) {
|
||||||
|
$address = new Address((int)$row['id_address']);
|
||||||
|
if (!Validate::isLoadedObject($address)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$originalPhone = $address->phone;
|
||||||
|
$originalMobile = $address->phone_mobile;
|
||||||
|
|
||||||
|
// Pass 'BATCH' context to the processing function
|
||||||
|
$this->processAddressNormalization($address, 'BATCH');
|
||||||
|
|
||||||
|
if ($address->phone !== $originalPhone || $address->phone_mobile !== $originalMobile) {
|
||||||
|
// Save only if a change was made
|
||||||
|
if ($address->save()) {
|
||||||
|
$updatedCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ['success' => true, 'updated_count' => $updatedCount];
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user