first commit

This commit is contained in:
O K
2025-09-05 21:35:59 +03:00
commit 849979d7e8
4 changed files with 417 additions and 0 deletions

303
phonenormalizer.php Normal file
View 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];
}
}