From 849979d7e8705bafa2ef63a28561ff0e203be8ca Mon Sep 17 00:00:00 2001 From: O K Date: Fri, 5 Sep 2025 21:35:59 +0300 Subject: [PATCH] first commit --- .gitignore | 2 + README.md | 96 ++++++++++++++ composer.json | 16 +++ phonenormalizer.php | 303 ++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 417 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 composer.json create mode 100644 phonenormalizer.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..dfd6caa --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..14522e7 --- /dev/null +++ b/README.md @@ -0,0 +1,96 @@ +# PrestaShop Phone Number Normalizer Module + +[![PHP Version](https://img.shields.io/badge/PHP-8.1-blue.svg?logo=PHP)] +[![PrestaShop Version](https://img.shields.io/badge/PrestaShop-8.x%20|%209.x-blue.svg?logo=prestashop)](https://www.prestashop.com) +[![License](https://img.shields.io/badge/license-AFL%203.0-green.svg)](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). \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b7e65bf --- /dev/null +++ b/composer.json @@ -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/" + } + } +} \ No newline at end of file diff --git a/phonenormalizer.php b/phonenormalizer.php new file mode 100644 index 0000000..2979586 --- /dev/null +++ b/phonenormalizer.php @@ -0,0 +1,303 @@ +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' => '

' . $this->l('Click the button below to attempt to normalize all existing phone numbers in your customer addresses database.') . '

' + . '

' . $this->l('Warning:') . ' ' . $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.') . '

' + . '

' . sprintf($this->l('A log of all changes will be saved to: %s'), '' . str_replace(_PS_ROOT_DIR_, '', $this->logFile) . '') . '

', + ], + ], + '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]; + } +} \ No newline at end of file