http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
-
+ client_max_body_size 128m;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
server {
listen 443 ssl;
listen [::]:443 ssl;
-
+ client_max_body_size 128m;
ssl_certificate /etc/ssl/certs/nginx.crt;
ssl_certificate_key /etc/ssl/private/nginx.key;
namespace yii_app\commands;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
use OpenAPI\Client\ObjectSerializer;
+use Symfony\Component\DomCrawler\Crawler;
+use voku\helper\HtmlDomParser;
use Yii;
use yii\console\Controller;
use yii\console\ExitCode;
return ExitCode::OK;
}
+
+ public function actionParseFlowwow()
+ {
+ {
+ $this->stdout("Начинаем парсинг Flowwow..." . PHP_EOL);
+
+ // URL для парсинга
+ $baseUrl = 'https://flowwow.com/shop/baza-cvetov-24f-6167/';
+
+ $this->stdout("Загружаем основную страницу: {$baseUrl}" . PHP_EOL);
+
+ // Контекст для обхода SSL
+ $context = stream_context_create([
+ "ssl" => [
+ "verify_peer" => false,
+ "verify_peer_name" => false,
+ ],
+ "http" => [
+ "header" => "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
+ "timeout" => 30
+ ]
+ ]);
+
+ try {
+ // Загружаем основную страницу
+ $dom = HtmlDomParser::file_get_html($baseUrl, false, $context);
+
+ $categoryWrappers = $dom->findMulti('div.category-wrapper') ?? [];
+ $categoryCount = count($categoryWrappers);
+
+ foreach ($categoryWrappers as $index => $category) {
+ $categoryNameElement = $category->findOne('h2.category-name');
+ $categoryName = $categoryNameElement ? trim((string)$categoryNameElement->text()) : 'Без категории';
+
+ $productCards = $category->findMulti('div.category-content-item a.product-card') ?? [];
+ $productCount = count($productCards);
+
+ foreach ($productCards as $productIndex => $card) {
+ $productPath = $card->getAttribute('href') ?: null;
+ if (!$productPath) { continue; }
+
+ $productUrl = 'https://flowwow.com' . $productPath;
+
+ $nameElement = $card->findOne('div.name');
+ $priceElement = $card->findOne('div.price span');
+ $imgElement = $card->findOne('img');
+
+ $productName = $nameElement ? trim((string)$nameElement->text()) : '';
+ $priceText = $priceElement ? trim((string)$priceElement->text()) : '';
+ $price = (int)preg_replace('/[^\d]/u', '', $priceText);
+ $imageUrl = $imgElement ? (string)$imgElement->getAttribute('src') : '';
+
+ $productData = [
+ 'category' => $categoryName,
+ 'name' => $productName,
+ 'price' => $price,
+ 'image_url' => $imageUrl,
+ 'product_url' => $productUrl
+ ];
+
+ try {
+ $this->stdout("Загружаем страницу товара..." . PHP_EOL);
+
+ // Загружаем страницу товара
+ $productDom = HtmlDomParser::file_get_html($productUrl, false, $context);
+ if ($productDom !== false) {
+ $this->stdout("Страница товара загружена, извлекаем данные..." . PHP_EOL);
+
+ // Извлечение описания
+ $descriptionElement = $productDom->findOne('div.product-card-content');
+ if ($descriptionElement) {
+ $productData['description'] = trim((string)$descriptionElement->text());
+ }
+
+ $properties = [];
+ foreach ($productDom->findMulti('div.property-item') ?? [] as $item) {
+ $propName = $item->findOne('div.property-name')?->text();
+ $propValue = $item->findOne('div.property-value')?->text();
+ if ($propName !== null && $propValue !== null) {
+ $properties[trim((string)$propName)] = trim((string)$propValue);
+ }
+ }
+ $productData['properties'] = $properties;
+
+ $this->stdout("Характеристики извлечены: " . count($properties) . " свойств" . PHP_EOL);
+
+ // $productDom->close();
+ }
+ } catch (\Exception $e) {
+ $this->stdout("Ошибка при загрузке страницы товара: " . $e->getMessage() . PHP_EOL);
+ $productData['error'] = 'Не удалось загрузить страницу товара: ' . $e->getMessage();
+ }
+
+ $products[] = $productData;
+ $this->stdout("Товар добавлен в результат: {$productName}" . PHP_EOL);
+
+ // Задержка между запросами
+ $delay = 500000; // 0.5 секунды
+ $this->stdout("Ждем {$delay} микросекунд перед следующим запросом..." . PHP_EOL);
+ usleep($delay);
+ }
+ }
+
+ // $dom->close();
+
+ $this->stdout("Парсинг завершен. Обработано товаров: " . count($products) . PHP_EOL);
+
+ // Сохраняем результат в файл (опционально)
+ $filename = date('Y-m-d_H-i-s') . '_flowwow_products.json';
+ file_put_contents($filename, json_encode($products, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
+ $this->stdout("Результат сохранен в файл: {$filename}" . PHP_EOL);
+
+ return ExitCode::OK;
+
+ } catch (\Exception $e) {
+ $this->stdout("Произошла ошибка: " . $e->getMessage() . PHP_EOL);
+ return ExitCode::SOFTWARE;
+ }
+ }
+ }
+
}
\ No newline at end of file
namespace app\controllers;
+use app\models\HtmlImportForm;
+use DOMDocument;
+use DOMXPath;
+use Exception;
+use GuzzleHttp\Client;
+use GuzzleHttp\Exception\RequestException;
+use Symfony\Component\DomCrawler\Crawler;
+use voku\helper\HtmlDomParser;
use Yii;
use yii\base\DynamicModel;
use yii\helpers\Json;
+use yii\web\UploadedFile;
+use yii_app\records\Images;
use yii_app\records\MatrixErp;
use yii_app\records\MatrixErpMedia;
use yii_app\records\MatrixErpProperty;
use yii\web\NotFoundHttpException;
use yii\filters\VerbFilter;
use yii_app\records\Products1c;
+use yii_app\services\FileService;
use yii_app\services\MarketplaceService;
+use yii_app\services\ProductParserService;
/**
* MatrixErpController implements the CRUD actions for MatrixErp model.
*/
class MatrixErpController extends Controller
{
+ const OUT_DIR =
+// __DIR__ . "/../json";
+ "/www/api2/json";
+
/**
* @inheritDoc
*/
);
}
- public static function fillInGuidsFromMarketplaceAndAdditional() {
+ public static function fillInGuidsFromMarketplaceAndAdditional()
+ {
$existings = MatrixErp::find()->all();
$existingMapGuidGroupNames = [];
foreach ($existings as $existing) {
$query = MatrixErp::find()
// ->select([])
->joinWith('matrixProperty')
- ->joinWith('matrixMedia')
-
- ;
+ ->joinWith('matrixMedia');
- if(Yii::$app->request->isPost && $filterModel->load(Yii::$app->request->post())){
+ if (Yii::$app->request->isPost && $filterModel->load(Yii::$app->request->post())) {
$nameFilter = $filterModel->nameFilter;
$groupNameFilter = $filterModel->groupNameFilter;
$activeFilter = $filterModel->activeFilter;
if (isset($nameFilter) && $nameFilter != "") {
if (!empty($nameFilter)) {
- $query->andFilterWhere(['like', 'matrix_erp.name', $nameFilter]);;
+ $query->andFilterWhere(['like', 'matrix_erp.name', $nameFilter]);;
}
}
$test = 1;
]);
}
- public function actionDelete($id) {
+ public function actionDelete($id)
+ {
$matrixErp = MatrixErp::findOne($id);
$matrixErp->active = 0;
$matrixErp->deleted_at = date('Y-m-d H:i:s');
throw new NotFoundHttpException('The requested page does not exist.');
}
-}
+
+
+ public function actionParseFlowwowCards()
+ {
+ $model = new HtmlImportForm();
+ $results = [];
+
+ if ($model->load(Yii::$app->request->post())) {
+ $model->files = UploadedFile::getInstances($model, 'files');
+
+ if ($model->validate()) {
+ foreach ($model->files as $file) {
+ $results[] = $this->processFile($file, $model->category);
+ }
+
+ Yii::$app->session->setFlash('importResults', $results);
+ return $this->refresh();
+ }
+ }
+
+ return $this->render('/matrix_erp/parse-flowwow-cards', [
+ 'model' => $model,
+ 'results' => Yii::$app->session->getFlash('importResults', []),
+ ]);
+ }
+
+ /**
+ * Обработка одного файла: парсинг → поиск MatrixErp по артикулу → upsert MatrixErpProperty по GUID.
+ */
+ protected function processFile(UploadedFile $file, string $category): array
+ {
+ $res = [
+ 'file' => $file->name,
+ 'status' => 'ok',
+ 'articule' => null,
+ 'guid' => null,
+ 'errors' => [],
+ ];
+
+ try {
+ $html = file_get_contents($file->tempName);
+
+ $service = new ProductParserService();
+
+ $parsed = $service->parseProductHtml($html);
+
+ $products = $this->normalizeParsed($parsed);
+ var_dump($products);die();
+ if (empty($products)) {
+ throw new \RuntimeException('Парсер вернул пустой результат.');
+ }
+
+ foreach ($products as $p) {
+ $art = $this->extractArticule($p['name'] ?? '');
+ $res['articule'] = $res['articule'] ?? $art;
+
+ if (!$art) {
+ $res['status'] = 'error';
+ $res['errors'][] = 'Артикул не найден в названии.';
+ continue;
+ }
+
+ $matrix = MatrixErp::find()->where(['articule' => $art])->one();
+ if (!$matrix) {
+ $res['status'] = 'error';
+ $res['errors'][] = "Товар с артикулом {$art} не найден в MatrixErp.";
+ continue;
+ }
+
+ $res['guid'] = $matrix->guid;
+
+ if (!$this->upsertProperty($matrix, $p, $category)) {
+ $res['status'] = 'error';
+ $res['errors'][] = "Не удалось сохранить свойства для GUID {$matrix->guid}.";
+ }
+ }
+ } catch (\Throwable $e) {
+ $res['status'] = 'error';
+ $res['errors'][] = $e->getMessage();
+ Yii::error($e->getMessage() . ' in ' . $file->name, __METHOD__);
+ }
+
+ return $res;
+ }
+
+
+ protected function normalizeParsed($parsed): array
+ {
+ if (!$parsed) return [];
+ if (is_array($parsed) && $this->isAssoc($parsed) && isset($parsed['name'])) {
+ return [$parsed];
+ }
+ return array_values((array)$parsed);
+ }
+
+ protected function isAssoc(array $arr): bool
+ {
+ // Аналог ArrayHelper::isAssociative для совместимости
+ if ([] === $arr) return false;
+ return array_keys($arr) !== range(0, count($arr) - 1);
+ }
+
+ /**
+ * Артикул из названия: ищем (FW0125) или просто FW0125, либо "Артикул: FW0125".
+ */
+ protected function extractArticule(string $name): ?string
+ {
+ $pattern = `/\\((?[A-Z]{2,4}\\d{3,6})\\)/u`;
+ if (preg_match($pattern, $name, $m)) {
+ return strtoupper($m['art']);
+ }
+
+ return null;
+ }
+
+ /**
+ * Upsert в matrix_erp_property по guid товара.
+ */
+ protected function upsertProperty(MatrixErp $matrix, array $matrixProduct, string $category, string $subcategory): bool
+ {
+ $prop = MatrixErpProperty::find()->where(['guid' => $matrix->guid])->one();
+ $nowDt = date('Y-m-d H:i:s');
+ $nowD = date('Y-m-d');
+
+ if (!$prop) {
+ $prop = new MatrixErpProperty();
+ $prop->guid = $matrix->guid;
+ $prop->matrix_erp_id = $matrix->guid;
+ $prop->date = $nowD;
+ $prop->created_admin_id = Yii::$app->user->id ?? null;
+ $prop->created_at = $nowDt;
+ } else {
+ $prop->updated_admin_id = Yii::$app->user->id ?? null;
+ $prop->updated_at = $nowDt;
+ }
+
+ try {
+ $uploadImage = FileService::downloadAsUploadedFile($externalUrl, maxBytes: 8_000_000);
+ } catch (\Throwable $e) {
+ Yii::error("Ошибка загрузки изображения по ссылке: " . $e->getMessage());
+
+ \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+ return ['error' => 'Не удалось скачать файл по ссылке'];
+ }
+
+
+ if ($uploadImage) {
+ if (Images::isImageFile($uploadImage, ['png', 'jpg', 'jpeg', 'webp', 'gif'])) {
+ $image = new Images();
+ $imageId = $image->loadImage($uploadImage); // ваш существующий метод (как при обычной загрузке)
+
+ if (!empty($imageId)) {
+ $prop->image_id = $imageId;
+ $prop->external_image_url = MarketplaceService::getProductImageUrl($imageId);
+ if (!empty($oldFile)) {
+ $oldFile->delete();
+ }
+ if (!$prop->save()) {
+ Yii::error("Ошибка сохранения ссылок на картинки " . json_encode($modelEdit->getErrors(), JSON_UNESCAPED_UNICODE));
+ }
+ }
+ } else {
+ \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+ return ['error' => 'Файл не является изображением либо запрещённое расширение'];
+ }
+ } else {
+ \Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+ return ['error' => 'Не передан файл и не указана ссылка'];
+ }
+
+
+ $prop->display_name = $matrixProduct['name'] ?? $prop->display_name;
+ $prop->description = $matrixProduct['description'] ?? $prop->description;
+ $prop->external_image_url = $matrixProduct['image_url'] ?? $prop->external_image_url;
+ $prop->product_url = $matrixProduct['product_url'] ?? $prop->product_url;
+
+ // Категории
+ $prop->flowwow_category = $category;
+ $prop->flowwow_subcategory = $category;
+ $prop->flowwow_subcategory = $matrixProduct['properties']['subcategory'] ?? $prop->flowwow_subcategory;
+ $prop->yandex_category = $matrixProduct['properties']['yandex_category'] ?? $prop->yandex_category;
+
+
+ if (isset($matrixProduct['properties']['length'])) $prop->length = (float)$matrixProduct['properties']['length'];
+ if (isset($matrixProduct['properties']['width'])) $prop->width = (float)$matrixProduct['properties']['width'];
+ if (isset($matrixProduct['properties']['height'])) $prop->height = (float)$matrixProduct['properties']['height'];
+ if (isset($matrixProduct['properties']['weight'])) $prop->weight = (float)$matrixProduct['properties']['weight'];
+
+ return $prop->save();
+ }
+}
\ No newline at end of file
--- /dev/null
+<?php
+namespace app\models;
+
+use yii\base\Model;
+use yii\web\UploadedFile;
+
+class HtmlImportForm extends Model
+{
+ public $category;
+ public $subcategory;
+ /** @var UploadedFile[] */
+ public $files;
+
+ public function rules(): array
+ {
+ return [
+ [['category', 'subcategory'], 'required'],
+ [['category', 'subcategory'], 'string', 'max' => 255],
+
+ [['files'], 'required'],
+ [
+ ['files'],
+ 'file',
+ 'skipOnEmpty' => false,
+ 'extensions' => ['html', 'htm'],
+ 'maxFiles' => 100,
+ ],
+ ];
+ }
+
+ public function attributeLabels(): array
+ {
+ return [
+ 'category' => 'Название категории',
+ 'subcategory' => 'Название подкатегории',
+ 'files' => 'HTML файлы',
+ ];
+ }
+}
+
namespace yii_app\services;
+use GuzzleHttp\Client;
use Yii;
use yii\helpers\ArrayHelper;
+use yii\helpers\FileHelper;
use yii\helpers\Url;
+
+use yii\web\UploadedFile;
use yii_app\helpers\ImageHelper;
use yii_app\records\Files;
use yii_app\records\Images;
return $result;
}
+ /**
+ * Скачивает файл по URL и возвращает "виртуальный" UploadedFile.
+ * Бросает исключение при ошибке/превышении лимита.
+ *
+ * @param string $url Ссылка на файл (http/https)
+ * @param int $maxBytes Максимальный размер, байт (например, 8 МБ)
+ * @param int $timeout Таймаут запроса, сек
+ */
+ public static function downloadAsUploadedFile(string $url, int $maxBytes = 8_000_000, int $timeout = 20): UploadedFile
+ {
+ $scheme = parse_url($url, PHP_URL_SCHEME);
+ if (!in_array($scheme, ['http', 'https'], true)) {
+ throw new \RuntimeException('Недопустимая схема URL');
+ }
+
+ $client = new Client([
+ 'timeout' => $timeout,
+ 'verify' => true,
+ 'headers' => [
+ 'User-Agent' => 'Mozilla/5.0 (compatible; Yii2 Guzzle Downloader)',
+ 'Accept' => 'image/*,*/*;q=0.8',
+ ],
+ ]);
+
+ $tmpDir = \Yii::getAlias('@runtime/http-downloads');
+ FileHelper::createDirectory($tmpDir);
+ $tmpFile = tempnam($tmpDir, 'img_');
+
+ $downloaded = 0;
+ $progress = function ($downloadTotal, $downloadedBytes) use (&$downloaded, $maxBytes) {
+ $downloaded = $downloadedBytes;
+ if ($downloaded > $maxBytes) {
+ throw new \RuntimeException('Файл слишком большой');
+ }
+ };
+
+ $response = $client->request('GET', $url, [
+ 'sink' => $tmpFile,
+ 'progress' => $progress,
+ ]);
+
+ if ($response->getStatusCode() !== 200) {
+ @unlink($tmpFile);
+ throw new \RuntimeException('HTTP ' . $response->getStatusCode());
+ }
+
+ $mime = FileHelper::getMimeType($tmpFile) ?: 'application/octet-stream';
+ if (strncmp($mime, 'image/', 6) !== 0) {
+ @unlink($tmpFile);
+ throw new \RuntimeException('Загруженный файл не является изображением');
+ }
+
+ $pathFromUrl = parse_url($url, PHP_URL_PATH) ?: '';
+ $basename = basename($pathFromUrl);
+ $name = $basename ?: ('image_' . uniqid());
+ $ext = pathinfo($name, PATHINFO_EXTENSION);
+
+ if ($ext === '') {
+ $map = [
+ 'image/jpeg' => 'jpg',
+ 'image/png' => 'png',
+ 'image/webp' => 'webp',
+ 'image/gif' => 'gif',
+ ];
+ $ext = $map[$mime] ?? 'jpg';
+ $name = $name . '.' . $ext;
+ }
+
+ /** @var UploadedFile $uploaded */
+ $uploaded = new UploadedFile([
+ 'name' => $name,
+ 'tempName' => $tmpFile,
+ 'type' => $mime,
+ 'size' => filesize($tmpFile),
+ 'error' => UPLOAD_ERR_OK,
+ ]);
+
+ return $uploaded;
+ }
+
}
--- /dev/null
+<?php
+
+namespace yii_app\services;
+use DOMDocument;
+use DOMXPath;
+
+class ProductParserService {
+ public function parseProductHtml(string $html): array
+ {
+
+ $dom = new DOMDocument();
+ libxml_use_internal_errors(true);
+ $dom->loadHTML($html);
+ libxml_clear_errors();
+
+ $xpath = new DOMXPath($dom);
+
+ return [
+ 'name' => $this->extractName($xpath),
+ 'price' => $this->extractPrice($xpath),
+ 'old_price' => $this->extractOldPrice($xpath),
+ 'rating' => $this->extractRating($xpath),
+ 'reviews_count' => $this->extractReviewsCount($xpath),
+ 'image_url' => $this->extractImageUrl($xpath),
+ 'description' => $this->extractDescription($xpath),
+ 'properties' => $this->extractProperties($xpath),
+
+ ];
+ }
+
+ private
+ function extractName(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//h1")->item(0);
+ return $node ? trim($node->nodeValue) : '';
+ }
+
+ private
+ function extractPrice(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//span[@class='footer-price']")->item(0);
+ return $node ? trim($node->nodeValue) : '';
+ }
+
+ private
+ function extractOldPrice(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//div[@class='footer-old-price']")->item(0);
+ return $node ? trim($node->nodeValue) : '';
+ }
+
+
+ private
+ function extractRating(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//div[@class='rating']//div[contains(@class, 'el-rate')]")->item(0);
+ return $node ? $node->getAttribute('data-score') : '';
+ }
+
+ private
+ function extractReviewsCount(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//div[@class='review-count']")->item(0);
+ return $node ? trim($node->nodeValue) : '';
+ }
+
+ private
+ function extractImageUrl(DOMXPath $xpath): string
+ {
+ $q1 = "//div[@id='js-detect-events']" .
+ "//div[contains(concat(' ', normalize-space(@class), ' '), ' swiper-slide-active ')]" .
+ "//img[contains(concat(' ', normalize-space(@class), ' '), ' main-image-content ')]";
+
+ $img = $xpath->query($q1)->item(0);
+
+ if (!$img) {
+ $q2 = "//div[@id='js-detect-events']//img[contains(concat(' ', normalize-space(@class), ' '), ' main-image-content ')]";
+ $img = $xpath->query($q2)->item(0);
+ }
+
+ $src = null;
+ if ($img) {
+ $src = $img->getAttribute('data-src');
+ }
+
+ if (!$src) {
+ $zoom = $xpath->query("//div[@id='js-image-zoom']")->item(0);
+ if ($zoom && preg_match('/background-image:\s*url\([\"\']?(.*?)[\"\']?\)/i', $zoom->getAttribute('style'), $m)) {
+ $src = $m[1];
+ }
+ }
+ return $src;
+ }
+
+ private
+ function extractDescription(DOMXPath $xpath): string
+ {
+ $node = $xpath->query("//div[@class='product-properties-general']" . "//div[@class='property-item']" . "//p[@class='pre-line']" . "//span")->item(0);
+ return $node ? trim($node->nodeValue) : '';
+ }
+
+ function extractProperties(DOMXPath $xpath): array
+ {
+ $properties = [];
+
+ $nodes = $xpath->query("//div[contains(concat(' ', normalize-space(@class), ' '), ' property-item ')]");
+
+ foreach ($nodes as $node) {
+
+ $nameNode = $xpath->query(".//h3[contains(concat(' ', normalize-space(@class), ' '), ' property-name ')]", $node)->item(0);
+ if (!$nameNode) {
+ continue;
+ }
+ $propName = trim($nameNode->textContent);
+
+ if (mb_stripos($propName, 'Размер') !== false) {
+ $sizeSpans = $xpath->query(
+ ".//li[contains(concat(' ', normalize-space(@class), ' '), ' size-item ')]" .
+ "//span[contains(concat(' ', normalize-space(@class), ' '), ' size-text ')]",
+ $node
+ );
+
+ $sizes = [];
+ foreach ($sizeSpans as $span) {
+ $text = trim(preg_replace('/\s+/u', ' ', $span->textContent));
+
+ if (preg_match('/(Ширина|Высота|Длина)\s*[-:–—]\s*(\d+(?:[.,]\d+)?)\s*см/ui', $text, $m)) {
+ $label = ucfirst(mb_strtolower($m[1]));
+ $value = (float) str_replace(',', '.', $m[2]);
+ $sizes[$label] = $value;
+ }
+ }
+
+ if ($sizes) {
+ $properties['Размер'] = $sizes;
+ if (isset($sizes['Ширина'])) $properties['width'] = $sizes['Ширина'];
+ if (isset($sizes['Высота'])) $properties['height'] = $sizes['Высота'];
+ }
+
+ continue;
+ }
+
+
+ $valueNode = $xpath->query(
+ ".//*[contains(concat(' ', normalize-space(@class), ' '), ' property-text ') " .
+ " or contains(concat(' ', normalize-space(@class), ' '), ' pre-line ')]",
+ $node
+ )->item(0);
+
+ if ($valueNode) {
+ $properties[$propName] = trim(preg_replace('/\s+/u', ' ', $valueNode->textContent));
+ }
+ }
+
+
+ if (!isset($properties['width']) || !isset($properties['height'])) {
+ $tagSpans = $xpath->query("//ul[contains(@class,'size-tag')]//li//span[not(contains(@class,'size-tag-icon'))]");
+ $seen = [];
+ foreach ($tagSpans as $span) {
+ $text = trim($span->textContent);
+ if (preg_match('/(\d+(?:[.,]\d+)?)\s*см/ui', $text, $m)) {
+ $val = (float) str_replace(',', '.', $m[1]);
+ $seen[] = $val;
+ }
+ }
+ if (!empty($seen)) {
+ if (!isset($properties['width']) && isset($seen[0])) $properties['width'] = $seen[0];
+ if (!isset($properties['height']) && isset($seen[1])) $properties['height'] = $seen[1];
+ }
+ }
+
+ return $properties;
+ }
+
+}
\ No newline at end of file
--- /dev/null
+<?php
+/** @var yii\web\View $this */
+/** @var app\models\HtmlImportForm $model */
+/** @var array $results */
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+
+$this->title = 'Импорт свойств из HTML';
+?>
+<div class="card">
+ <div class="card-body">
+ <h3><?= Html::encode($this->title) ?></h3>
+
+ <?php $form = ActiveForm::begin(['options' => ['enctype' => 'multipart/form-data']]); ?>
+
+ <?= $form->field($model, 'category')->textInput(['maxlength' => true, 'placeholder' => 'Например: Цветы']) ?>
+ <?= $form->field($model, 'subcategory')->textInput(['maxlength' => true, 'placeholder' => 'Например: Монобукеты']) ?>
+
+ <?= $form->field($model, 'files[]')->fileInput([
+ 'multiple' => true,
+ 'accept' => '.html,.htm',
+ ]) ?>
+
+ <div class="form-group">
+ <?= Html::submitButton('Сохранить', ['class' => 'btn btn-primary']) ?>
+ </div>
+
+ <?php ActiveForm::end(); ?>
+ </div>
+</div>
+
+<?php if (!empty($results)): ?>
+ <div class="card mt-3">
+ <div class="card-body">
+ <h5>Результаты импорта</h5>
+ <div class="table-responsive">
+ <table class="table table-sm table-bordered align-middle">
+ <thead>
+ <tr>
+ <th>Файл</th>
+ <th>Артикул</th>
+ <th>GUID</th>
+ <th>Статус</th>
+ <th>Ошибки</th>
+ </tr>
+ </thead>
+ <tbody>
+ <?php foreach ($results as $r): ?>
+ <tr>
+ <td><?= Html::encode($r['file']) ?></td>
+ <td><?= Html::encode($r['articule'] ?? '') ?></td>
+ <td><?= Html::encode($r['guid'] ?? '') ?></td>
+ <td>
+ <?php if (($r['status'] ?? 'ok') === 'ok'): ?>
+ <span class="badge bg-success">ok</span>
+ <?php else: ?>
+ <span class="badge bg-danger">error</span>
+ <?php endif; ?>
+ </td>
+ <td>
+ <?php if (!empty($r['errors'])): ?>
+ <ul class="mb-0">
+ <?php foreach ($r['errors'] as $e): ?>
+ <li><?= Html::encode($e) ?></li>
+ <?php endforeach; ?>
+ </ul>
+ <?php endif; ?>
+ </td>
+ </tr>
+ <?php endforeach; ?>
+ </tbody>
+ </table>
+ </div>
+ </div>
+ </div>
+<?php endif; ?>
function monthList()
{
$list = [];
- $start = new DateTime('2025-01');
+ $start = new DateTime('2024-01');
$end = new DateTime('2026-12');
while ($start <= $end) {
$key = $start->format('Y-m');