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]; } }