Реалізація валідації даних при імпорті товарів

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Реалізація валідації даних при імпорті товарів
Середня
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Реалізація валідації даних при імпорті товарів

Дані від постачальників — це неконтрольований вхід. У прайсі можуть бути негативні ціни, порожні артикули, невірні одиниці виміру, XSS у описах та рядки замість чисел. Без валідації це попадає прямо в каталог. Хороша система валідації затримує мусор на вході і надає чіткий звіт про проблеми.

Два рівні валідації

Структурна валідація — перевіряє формат і типи даних:

  • Обов'язкові поля присутні
  • Числа є числами
  • Дати в правильному форматі
  • Довжина рядків у межах допустимого

Бізнес-валідація — перевіряє смислові правила:

  • Ціна не перевищує розумну межу
  • SKU унікальний у межах джерела
  • Категорія існує в системі
  • Зміна ціни не перевищує допустимий відсоток

Побудова валідатора на основі Laravel Validator

class ProductImportValidator
{
    private array $rules = [
        'sku'         => ['required', 'string', 'max:100'],
        'name'        => ['required', 'string', 'max:500'],
        'price'       => ['required', 'numeric', 'min:0.01', 'max:100000000'],
        'qty'         => ['nullable', 'integer', 'min:0', 'max:9999999'],
        'description' => ['nullable', 'string', 'max:100000'],
        'category'    => ['nullable', 'string', 'max:300'],
        'images'      => ['nullable', 'array', 'max:20'],
        'images.*'    => ['url', 'max:2000'],
        'weight'      => ['nullable', 'numeric', 'min:0', 'max:10000'],
    ];

    public function validate(array $row): ValidationResult
    {
        $validator = \Illuminate\Support\Facades\Validator::make(
            $row,
            $this->rules,
            $this->customMessages()
        );

        $errors = [];
        if ($validator->fails()) {
            $errors = $validator->errors()->toArray();
        }

        // Бізнес-правила
        $errors = array_merge($errors, $this->applyBusinessRules($row));

        return new ValidationResult(
            valid: empty($errors),
            errors: $errors,
            data: $validator->validated(),
        );
    }

    private function applyBusinessRules(array $row): array
    {
        $errors = [];

        // Перевірка аномального зміни ціни
        if (!empty($row['sku']) && !empty($row['price'])) {
            $existing = Product::where('sku', $row['sku'])->value('price');
            if ($existing && $existing > 0) {
                $change = abs($row['price'] - $existing) / $existing;
                if ($change > 0.5) {
                    $errors['price'][] = "Price change {$change}% exceeds 50% threshold";
                }
            }
        }

        // Перевірка на XSS у описі
        if (!empty($row['description'])) {
            $clean = strip_tags($row['description']);
            if ($clean !== $row['description']) {
                $errors['description'][] = 'HTML tags detected in description';
            }
        }

        return $errors;
    }
}

Санітизація даних перед валідацією

Спочатку очищаємо очевидний мусор, потім валідуємо:

class ProductDataSanitizer
{
    public function sanitize(array $raw): array
    {
        return [
            'sku'         => $this->cleanString($raw['sku'] ?? ''),
            'name'        => $this->cleanString($raw['name'] ?? ''),
            'price'       => $this->parseDecimal($raw['price'] ?? null),
            'qty'         => $this->parseInt($raw['qty'] ?? null),
            'description' => $this->sanitizeHtml($raw['description'] ?? ''),
            'weight'      => $this->parseDecimal($raw['weight'] ?? null),
            'images'      => $this->parseImageUrls($raw['images'] ?? []),
        ];
    }

    private function cleanString(?string $value): string
    {
        if ($value === null) return '';
        $value = trim($value);
        $value = preg_replace('/\p{C}/u', '', $value); // невидимі символи
        return mb_substr($value, 0, 1000);
    }

    private function parseDecimal(mixed $value): ?float
    {
        if ($value === null || $value === '') return null;
        $value = str_replace([' ', ',', "\xc2\xa0"], ['', '.', ''], (string) $value);
        return is_numeric($value) ? (float) $value : null;
    }

    private function parseInt(mixed $value): ?int
    {
        $decimal = $this->parseDecimal($value);
        return $decimal !== null ? (int) $decimal : null;
    }

    private function sanitizeHtml(?string $html): string
    {
        if (!$html) return '';
        // Дозволяємо лише безпечні теги
        return strip_tags($html, '<p><br><b><strong><i><em><ul><ol><li>');
    }

    private function parseImageUrls(mixed $raw): array
    {
        if (is_string($raw)) {
            $raw = array_map('trim', explode(',', $raw));
        }
        return array_values(array_filter(
            (array) $raw,
            fn($url) => filter_var($url, FILTER_VALIDATE_URL)
        ));
    }
}

Пакетна дедуплікація SKU у межах імпорту

Один файл може містити дублюючиеся артикули (постачальник склеїв кілька прайсів):

class ImportDeduplicator
{
    private array $seenSkus = [];

    public function check(string $sku, int $lineNumber): ?string
    {
        if (isset($this->seenSkus[$sku])) {
            return "Duplicate SKU '{$sku}' at line {$lineNumber}, first seen at line {$this->seenSkus[$sku]}";
        }
        $this->seenSkus[$sku] = $lineNumber;
        return null;
    }
}

Стратегії обробки помилок

Не всі помилки одинакові — потрібно розділити їх за критичністю:

enum ValidationSeverity: string
{
    case CRITICAL = 'critical'; // рядок пропускається
    case WARNING  = 'warning';  // рядок імпортується з прапором
    case INFO     = 'info';     // лише в журнал
}

Правила з критичністю:

Правило Критичність
Порожній SKU CRITICAL
Відсутня назва CRITICAL
Ціна = 0 або негативна CRITICAL
Ціна змінилася >50% WARNING
HTML у описі WARNING
Категорія не знайдена INFO
Битий URL зображення INFO

Збір і зберігання помилок

class ImportErrorCollector
{
    private array $errors = [];
    private int   $criticalCount = 0;

    public function add(int $line, string $sku, string $field, string $message, ValidationSeverity $severity): void
    {
        $this->errors[] = compact('line', 'sku', 'field', 'message', 'severity');
        if ($severity === ValidationSeverity::CRITICAL) {
            $this->criticalCount++;
        }
    }

    public function persist(int $importId): void
    {
        if (empty($this->errors)) return;

        // Записуємо пакетами по 1000 рядків
        foreach (array_chunk($this->errors, 1000) as $chunk) {
            ImportError::insert(array_map(
                fn($e) => array_merge($e, ['import_id' => $importId, 'severity' => $e['severity']->value]),
                $chunk
            ));
        }

        ImportRun::find($importId)->update([
            'errors_count'          => count($this->errors),
            'critical_errors_count' => $this->criticalCount,
        ]);
    }
}

Поріг скасування імпорту

Якщо забагато критичних помилок — зупиняємо імпорт повністю:

private const ABORT_THRESHOLD = 0.20; // 20% критичних — стоп

if ($collector->getCriticalRate() > self::ABORT_THRESHOLD) {
    throw new ImportAbortedException(
        "Too many critical errors: {$collector->getCriticalRate()}%"
    );
}

Терміни реалізації

  • Структурна валідація (Laravel Validator), санітизація, збір помилок — 1 день
  • Бізнес-правила, дедуплікація SKU, рівні severity — +1 день
  • Зберігання помилок в БД, поріг скасування, відображення в admin UI — +1 день