Як видалити старі зображення товарів в Opencart?

11.05.2026 | Андрей
Як видалити старі зображення товарів в Opencart?

При роботі із зображеннями опенкарт має 1 недолік. Видаливши товар, зображення залишається на сервері. Якщо е сервер то можна ігнорувати, але якщо це хостинг із обмеженнями інодів то є сенс очистити старі файли.

Я покажу простий приклад на базі 2 скриптів для очищення старих файлів.

Пам'ятайте: актуальний бекап рятував не одну людину. Створіть бекап файлів та бд перед початком.

Скрипт php для копіювання фотографій до нової папки

Скрипт копіює фотографії активних товарів до нової папки та змінює назву по коду товару (sku).

Всі секції коду мають коментарі, щоб запустити: створіть папку в корневій дерикторії сайту та створіть там скрипт migrate_images.php із цим вмістом

Відмічу особливість: в скрипті є масив $extraImageFields, в моєму кейсі це додаткові поля із таблиці oc_product, якщо у вас таких немає - очистіть цей масив.




/**
 * Скрипт міграції зображень OpenCart 3
 *
 * Що робить:
 * - Проходить всі активні товари
 * - Копіює зображення в нову папку
 * - Оновлює шляхи в БД
 * - Поля image1..image5 переносить в ap_product_image
 * - Перевіряє існування файлів
 * - Логує всі дії
 * - Генерує JSON список старих файлів для подальшого видалення
 *
 * cli Запуск:
 * /usr/bin/php або php migrate_images.php
 */

ini_set('display_errors', 1);
error_reporting(E_ALL);
set_time_limit(0);

require '../config.php';
require '../system/library/db/mysqli.php';

$db = new db\MySQLi(DB_HOSTNAME, DB_USERNAME, DB_PASSWORD, DB_DATABASE);

/**
 * ===========================================
 * НАЛАШТУВАННЯ
 * ===========================================
 */

// Нова папка всередині image/
$newImageFolder = 'catalog/new-products/';

// Файл логів
$logFile = __DIR__ . '/migrate_images.log';

// JSON файл зі старими файлами
$jsonDeleteFile = __DIR__ . '/old_images_to_delete.json';

// Таблиці
$productTable = 'ap_product';
$productImageTable = 'ap_product_image';


// ! Важливо, якщо використовується стандартна таблиця product - очистіть цей масив !
// Поля додаткових зображень
$extraImageFields = [
    'image1',
    'image2',
    'image3',
    'image4',
    'image5'
];

/**
 * ===========================================
 * ДОПОМІЖНІ ФУНКЦІЇ
 * ===========================================
 */

function logMessage($message)
{
    global $logFile;

    $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;

    echo $line;
	echo "\n";

    file_put_contents($logFile, $line, FILE_APPEND);
}

function normalizePath($path)
{
    $path = trim($path);

    $path = str_replace('\\', '/', $path);
    $path = preg_replace('#/+#', '/', $path);

    return ltrim($path, '/');
}

function ensureDirectory($path)
{
    if (!is_dir($path)) {
        mkdir($path, 0777, true);
    }
}

/**
 * Генерація унікального імені файла
 *
 * Приклад:
 * ABC123_1.jpg
 * ABC123_2.jpg
 * ABC123_3.webp
 */
function generateUniqueFilename($destinationDir, $sku, $extension, $startIndex = 1)
{
    // Очищаємо SKU від небажаних символів
    $sku = trim($sku);

    $sku = preg_replace('/[^a-zA-Z0-9_-]/', '_', $sku);

    if (!$sku) {
        $sku = 'product';
    }

    // Нормалізуємо extension
    $extension = ltrim(strtolower($extension), '.');

    $index = (int)$startIndex;

    do {

        $newFilename = $sku . '_' . $index;

        if ($extension) {
            $newFilename .= '.' . $extension;
        }

        $fullPath = rtrim($destinationDir, '/') . '/' . $newFilename;

        $index++;

    } while (file_exists($fullPath));

    return $newFilename;
}

function copyImageToNewFolder(
    $oldRelativePath,
    $newFolder,
    $sku,
    $index = 1
)
{
    $oldRelativePath = normalizePath($oldRelativePath);

    $oldFullPath = DIR_IMAGE . $oldRelativePath;

    if (!is_file($oldFullPath)) {
        return [
            'success' => false,
            'error' => 'Файл не існує: ' . $oldFullPath
        ];
    }

    $info = pathinfo($oldRelativePath);

    $extension = $info['extension'] ?? '';

    $destinationDir = DIR_IMAGE . normalizePath($newFolder);

    ensureDirectory($destinationDir);

    $newFilename = generateUniqueFilename(
        $destinationDir,
        $sku,
        $extension,
        $index
    );

    $newRelativePath = normalizePath($newFolder . '/' . $newFilename);

    $newFullPath = DIR_IMAGE . $newRelativePath;

    if (!copy($oldFullPath, $newFullPath)) {
        return [
            'success' => false,
            'error' => 'Не вдалося скопіювати файл'
        ];
    }

    return [
        'success' => true,
        'old_relative' => $oldRelativePath,
        'new_relative' => $newRelativePath,
        'old_full' => $oldFullPath,
        'new_full' => $newFullPath
    ];
}

/**
 * ===========================================
 * ПОЧАТОК
 * ===========================================
 */

logMessage('=== СТАРТ МІГРАЦІЇ ЗОБРАЖЕНЬ ===');

$filesToDelete = [];

/**
 * ===========================================
 * ОТРИМАННЯ АКТИВНИХ ТОВАРІВ
 * ===========================================
 */

$sql = "
    SELECT *
    FROM `{$productTable}`
    WHERE status = 1
";

$result = $db->query($sql);

if (!$result->num_rows) {
    logMessage('Активні товари не знайдені');
    exit;
}

logMessage('Знайдено товарів: ' . $result->num_rows);

/**
 * ===========================================
 * ОБРОБКА ТОВАРІВ
 * ===========================================
 */

foreach ($result->rows as $product) {

    $productId = (int)$product['product_id'];

    logMessage("=== ТОВАР ID: {$productId} ===");

    /**
     * ---------------------------------------
     * ГОЛОВНЕ ЗОБРАЖЕННЯ
     * ---------------------------------------
     */
	
	$imageIndex = 1;
	
    if (!empty($product['image'])) {

        $copyResult = copyImageToNewFolder(
			$product['image'],
			$newImageFolder,
			$product['sku'],
			$imageIndex++
		);

        if ($copyResult['success']) {

            $newImage = $db->escape($copyResult['new_relative']);

            $db->query("
                UPDATE `{$productTable}`
                SET image = '{$newImage}'
                WHERE product_id = {$productId}
            ");

            $filesToDelete[] = $copyResult['old_relative'];

            logMessage(
                "Головне зображення оновлено: " .
                $copyResult['old_relative'] .
                ' => ' .
                $copyResult['new_relative']
            );

        } else {

            logMessage(
                'ПОМИЛКА: ' . $copyResult['error']
            );
        }
    }
	
	/**
     * ---------------------------------------
     * AP_PRODUCT_IMAGE
     * ---------------------------------------
     */

    $productImages = $db->query("
        SELECT *
        FROM `{$productImageTable}`
        WHERE product_id = {$productId}
    ");

    if ($productImages->num_rows) {

        foreach ($productImages->rows as $imageRow) {

            if (empty($imageRow['image'])) {
                continue;
            }

			$copyResult = copyImageToNewFolder(
				$product['image'],
				$newImageFolder,
				$product['sku'],
				$imageIndex++
			);

            if (!$copyResult['success']) {

                logMessage(
                    "ПОМИЛКА ap_product_image ID {$imageRow['product_image_id']}: " .
                    $copyResult['error']
                );

                continue;
            }

            $newImage = $db->escape($copyResult['new_relative']);

            $db->query("
                UPDATE `{$productImageTable}`
                SET image = '{$newImage}'
                WHERE product_image_id = " . (int)$imageRow['product_image_id']
            );

            $filesToDelete[] = $copyResult['old_relative'];

            logMessage(
                "ap_product_image оновлено: " .
                $copyResult['old_relative'] .
                ' => ' .
                $copyResult['new_relative']
            );
        }
    }

    /**
     * ---------------------------------------
     * ДОДАТКОВІ IMAGE1..IMAGE5
     * ---------------------------------------
     */

    $sortOrder = 0;

    foreach ($extraImageFields as $field) {

        if (empty($product[$field])) {
            continue;
        }

		$copyResult = copyImageToNewFolder(
			$product[$field],
			$newImageFolder,
			$product['sku'],
			$imageIndex++
		);
		

        if (!$copyResult['success']) {

            logMessage(
                "ПОМИЛКА {$field}: " . $copyResult['error']
            );

            continue;
        }

        $newImage = $db->escape($copyResult['new_relative']);

        /**
         * Додаємо в ap_product_image
         */
        $db->query("
            INSERT INTO `{$productImageTable}`
            SET
                product_id = {$productId},
                image = '{$newImage}',
                sort_order = {$sortOrder}
        ");

        /**
         * Очищаємо старе поле
         */
        $db->query("
            UPDATE `{$productTable}`
            SET {$field} = ''
            WHERE product_id = {$productId}
        ");

        $filesToDelete[] = $copyResult['old_relative'];

        logMessage(
            "{$field} перенесено в {$productImageTable}: " .
            $copyResult['new_relative']
        );

        $sortOrder++;
    }

    
}

/**
 * ===========================================
 * ЗАПИС JSON
 * ===========================================
 */

$filesToDelete = array_values(array_unique($filesToDelete));

file_put_contents(
    $jsonDeleteFile,
    json_encode(
        $filesToDelete,
        JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE
    )
);

logMessage('JSON файл створено: ' . $jsonDeleteFile);

logMessage('Файлів для видалення: ' . count($filesToDelete));

logMessage('=== МІГРАЦІЮ ЗАВЕРШЕНО ===');


Видалення старих файлів

Тепер видалимо старі файли та папки, які 100% мають не потрібні фото товарів.

Важливо: скрипт має масив $allowedFolders додайте папки, які потрібно перевірити та видалити.

Лоігка запуску аналогічна: створюємо файл delete_old_images.php та запускаємо після переміщення зображень.




/**
 * Повне очищення старих папок із зображеннями
 *
 * Логіка:
 * 1. Видаляємо файли з JSON
 * 2. Після цього проходимо ВСІ дозволені папки
 * 3. Видаляємо будь-які залишкові файли
 * 4. Видаляємо порожні папки
 *
 * ВАЖЛИВО:
 * Скрипт чіпає ТІЛЬКИ папки з $allowedFolders
 */

ini_set('display_errors', 1);
error_reporting(E_ALL);
set_time_limit(0);

require '../config.php';

/**
 * ===========================================
 * НАЛАШТУВАННЯ
 * ===========================================
 */

$jsonFile = __DIR__ . '/old_images_to_delete.json';

$logFile = __DIR__ . '/delete_old_images.log';

/**
 * ДОЗВОЛЕНІ ПАПКИ
 */
$allowedFolders = [

    'catalog/demo/',
    'catalog/ndir/',
    'catalog/products/',
    'catalog/products_image/',
    'catalog/nprod26/',
    'catalog/hcupd/',
];

/**
 * ===========================================
 * ДОПОМІЖНІ ФУНКЦІЇ
 * ===========================================
 */

function logMessage($message)
{
    global $logFile;

    $line = '[' . date('Y-m-d H:i:s') . '] ' . $message . PHP_EOL;

    echo $line;

    file_put_contents(
        $logFile,
        $line,
        FILE_APPEND
    );
}

function normalizePath($path)
{
    $path = str_replace('\\', '/', $path);

    $path = preg_replace('#/+#', '/', $path);

    return trim($path, '/');
}

function removeEmptyDirectories($dir, $stopDir)
{
    $dir = rtrim($dir, '/');
    $stopDir = rtrim($stopDir, '/');

    while ($dir !== $stopDir) {

        if (!is_dir($dir)) {
            return;
        }

        $files = array_diff(scandir($dir), ['.', '..']);

        if (!empty($files)) {
            return;
        }

        if (@rmdir($dir)) {

            logMessage('Видалено порожню папку: ' . $dir);

            $dir = dirname($dir);

        } else {
            return;
        }
    }
}

/**
 * Рекурсивне видалення ВСІХ файлів
 */
function removeAllFilesRecursive($dir)
{
    if (!is_dir($dir)) {
        return;
    }

    $iterator = new RecursiveIteratorIterator(
        new RecursiveDirectoryIterator(
            $dir,
            RecursiveDirectoryIterator::SKIP_DOTS
        ),
        RecursiveIteratorIterator::CHILD_FIRST
    );

    foreach ($iterator as $item) {

        $path = $item->getPathname();

        if ($item->isFile()) {

            if (@unlink($path)) {

                logMessage(
                    'Видалено залишковий файл: ' . $path
                );

            } else {

                logMessage(
                    'ПОМИЛКА видалення файла: ' . $path
                );
            }

        } elseif ($item->isDir()) {

            $files = array_diff(scandir($path), ['.', '..']);

            if (empty($files)) {

                if (@rmdir($path)) {

                    logMessage(
                        'Видалено порожню папку: ' . $path
                    );
                }
            }
        }
    }

    /**
     * Видалення кореневої папки
     */
    $files = array_diff(scandir($dir), ['.', '..']);

    if (empty($files)) {

        if (@rmdir($dir)) {

            logMessage(
                'Видалено кореневу папку: ' . $dir
            );
        }
    }
}

/**
 * ===========================================
 * ВИДАЛЕННЯ ФАЙЛІВ З JSON
 * ===========================================
 */

if (is_file($jsonFile)) {

    $json = file_get_contents($jsonFile);

    $files = json_decode($json, true);

    if (is_array($files)) {

        logMessage(
            'Файлів у JSON: ' . count($files)
        );

        foreach ($files as $relativePath) {

            $relativePath = normalizePath($relativePath);

            $fullPath = rtrim(DIR_IMAGE, '/') . '/' . $relativePath;

            if (!is_file($fullPath)) {
                continue;
            }

            if (@unlink($fullPath)) {

                logMessage(
                    'Видалено файл із JSON: ' . $relativePath
                );

                removeEmptyDirectories(
                    dirname($fullPath),
                    rtrim(DIR_IMAGE, '/')
                );

            } else {

                logMessage(
                    'ПОМИЛКА видалення: ' . $relativePath
                );
            }
        }

    } else {

        logMessage('Некоректний JSON');
    }

} else {

    logMessage('JSON файл не знайдено');
}

/**
 * ===========================================
 * ПОВНЕ ОЧИЩЕННЯ ДОЗВОЛЕНИХ ПАПОК
 * ===========================================
 */

logMessage('===================================');
logMessage('ПОЧАТОК ПОВНОГО ОЧИЩЕННЯ ПАПОК');

foreach ($allowedFolders as $folder) {

    $folder = normalizePath($folder);

    $fullFolderPath = rtrim(DIR_IMAGE, '/') . '/' . $folder;

    if (!is_dir($fullFolderPath)) {

        logMessage(
            'Папка не існує: ' . $fullFolderPath
        );

        continue;
    }

    logMessage(
        'Очищення папки: ' . $fullFolderPath
    );

    removeAllFilesRecursive($fullFolderPath);
}

/**
 * ===========================================
 * ФІНАЛ
 * ===========================================
 */

logMessage('===================================');

logMessage('ОЧИЩЕННЯ ЗАВЕРШЕНО');


Бажаєте дізнатись скількі коштує Ваш проєкт?

Заповніть заявку

Категорії

Те, що читають:

Name post

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco

Name post

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco