, attr:int}> */ private array $props; /** @var array GUID типа цены => название */ private array $priceTypes; public function __construct() { $props = get_option(self::PROPS_OPTION, []); $this->props = is_array($props) ? $props : []; $types = get_option(self::PRICES_OPTION, []); $this->priceTypes = is_array($types) ? $types : []; } public function importClassifier(SimpleXMLElement $classifier): void { foreach (Xml::nodes($classifier, 'Группы') as $groups) { $this->importGroups($groups, 0); } $properties = [ ...Xml::nodes($classifier, 'Свойства', 'Свойство'), ...Xml::nodes($classifier, 'Свойства', 'СвойствоНоменклатуры'), ]; foreach ($properties as $property) { $this->importProperty($property); } if ($properties) { update_option(self::PROPS_OPTION, $this->props, false); } // В CommerceML 2.08+ типы цен лежат внутри классификатора offers.xml. foreach (Xml::nodes($classifier, 'ТипыЦен') as $priceTypes) { $this->importPriceTypes($priceTypes); } Log::debug(sprintf('Классификатор: свойств %d', count($properties))); } public function importPriceTypes(SimpleXMLElement $node): void { foreach (Xml::nodes($node, 'ТипЦены') as $type) { $guid = Xml::text($type, 'Ид'); if ($guid !== '') { $this->priceTypes[$guid] = Xml::text($type, 'Наименование'); } } update_option(self::PRICES_OPTION, $this->priceTypes, false); } /** * @return array{name:string, values:array, attr:int}|null */ public function property(string $guid): ?array { return $this->props[$guid] ?? null; } /** * Отпечаток справочников: если в 1С переименовали значение свойства, товары надо обработать заново. */ public function signature(): string { return md5(serialize($this->props)); } /** * ID атрибутов WooCommerce, которыми управляет обмен (остальные на товаре не трогаем). * * @return list */ public function managedAttributeIds(): array { return array_values(array_filter(array_map(static fn(array $p): int => (int) $p['attr'], $this->props))); } /** * Подходит ли цена с типом $typeId под настройку (Ид или название типа цены). */ public function priceTypeMatches(string $typeId, string $wanted): bool { return $typeId === $wanted || mb_strtolower($this->priceTypes[$typeId] ?? '') === mb_strtolower($wanted); } /** * Атрибут, созданный в этом же запросе, WooCommerce зарегистрирует только при следующей загрузке. */ public static function ensureTaxonomy(string $taxonomy): void { if ($taxonomy !== '' && !taxonomy_exists($taxonomy)) { register_taxonomy($taxonomy, ['product'], [ 'hierarchical' => false, 'public' => false, 'show_ui' => false, 'query_var' => false, 'rewrite' => false, ]); } } // --- Группы --------------------------------------------------------------- private function importGroups(SimpleXMLElement $groups, int $parent): void { foreach (Xml::nodes($groups, 'Группа') as $group) { $guid = Xml::text($group, 'Ид'); $name = Xml::text($group, 'Наименование'); if ($guid === '' || $name === '') { continue; } $termId = $this->ensureCategory($guid, $name, $parent); foreach (Xml::nodes($group, 'Группы') as $children) { $this->importGroups($children, $termId); } } } private function ensureCategory(string $guid, string $name, int $parent): int { $id = Repo::termId($guid, 'product_cat'); if ($id > 0) { $term = get_term($id, 'product_cat'); // WordPress хранит «&» в названиях терминов как «&». $changed = $term instanceof \WP_Term && (html_entity_decode($term->name, ENT_QUOTES | ENT_HTML5, 'UTF-8') !== $name || (int) $term->parent !== $parent); if ($changed && Settings::bool('update_categories')) { wp_update_term($id, 'product_cat', ['name' => $name, 'parent' => $parent]); } return $id; } $result = wp_insert_term($name, 'product_cat', ['parent' => $parent]); if (is_wp_error($result)) { // Категория с таким названием уже есть (например, создана вручную) — привязываем её. $id = (int) $result->get_error_data('term_exists'); if ($id <= 0) { Log::warning('Категория «' . $name . '» не создана: ' . $result->get_error_message()); return 0; } } else { $id = (int) $result['term_id']; Lang::assignDefaultTerm($id, 'product_cat'); } update_term_meta($id, Repo::META, $guid); Repo::rememberTerm($guid, 'product_cat', $id); return $id; } // --- Свойства ------------------------------------------------------------- private function importProperty(SimpleXMLElement $property): void { $guid = Xml::text($property, 'Ид'); $name = Xml::text($property, 'Наименование'); if ($guid === '' || $name === '') { return; } $values = $this->props[$guid]['values'] ?? []; foreach (Xml::nodes($property, 'ВариантыЗначений', 'Справочник') as $variant) { $id = Xml::text($variant, 'ИдЗначения'); $value = Xml::text($variant, 'Значение'); if ($id !== '' && $value !== '') { $values[$id] = $value; } } $attr = (int) ($this->props[$guid]['attr'] ?? 0); if (Settings::bool('import_attributes')) { $attr = $this->ensureAttribute($guid, $name, $attr); } $this->props[$guid] = ['name' => $name, 'values' => $values, 'attr' => $attr]; } private function ensureAttribute(string $guid, string $name, int $attrId): int { if ($attrId > 0 && wc_get_attribute($attrId)) { return $attrId; } // Уже есть атрибут с таким названием (например, создан вручную) — используем его. foreach (wc_get_attribute_taxonomies() as $tax) { if (mb_strtolower($tax->attribute_label) === mb_strtolower($name)) { return (int) $tax->attribute_id; } } $args = ['name' => mb_substr($name, 0, 190), 'type' => 'select', 'order_by' => 'menu_order', 'has_archives' => false]; $id = wc_create_attribute($args + ['slug' => self::slug($name)]); if (is_wp_error($id)) { $id = wc_create_attribute($args + ['slug' => 'c1-' . substr(md5($guid), 0, 12)]); } if (is_wp_error($id)) { Log::warning('Атрибут «' . $name . '» не создан: ' . $id->get_error_message()); return 0; } self::ensureTaxonomy(wc_attribute_taxonomy_name_by_id((int) $id)); return (int) $id; } private static function slug(string $name): string { static $map = [ 'а' => 'a', 'б' => 'b', 'в' => 'v', 'г' => 'g', 'д' => 'd', 'е' => 'e', 'ё' => 'e', 'ж' => 'zh', 'з' => 'z', 'и' => 'i', 'й' => 'y', 'к' => 'k', 'л' => 'l', 'м' => 'm', 'н' => 'n', 'о' => 'o', 'п' => 'p', 'р' => 'r', 'с' => 's', 'т' => 't', 'у' => 'u', 'ф' => 'f', 'х' => 'h', 'ц' => 'c', 'ч' => 'ch', 'ш' => 'sh', 'щ' => 'sch', 'ъ' => '', 'ы' => 'y', 'ь' => '', 'э' => 'e', 'ю' => 'yu', 'я' => 'ya', 'і' => 'i', 'ї' => 'yi', 'є' => 'ye', 'ґ' => 'g', ]; $slug = strtr(mb_strtolower($name), $map); $slug = trim((string) preg_replace('/[^a-z0-9]+/', '-', $slug), '-'); // Ограничение WooCommerce на длину slug атрибута — 28 символов вместе с «pa_». return trim(substr($slug, 0, 25), '-') ?: 'c1-attr'; } }