modImporter. Настройка импорта в minishop2 из XLSX

В продолжение прошлой статьи, предлагаю разобрать очередной пример импорта в минишоп, на этот раз из Экселя (XLSX). В этот раз не будет картинок и прочих деталей (их выше крыше было в прошлой статье), а просто рассмотрим конечный код процессора. Так меньше по объему и наверняка так будет многим более понятно.

И так, не считая контроллера в пару строк, при настройке импорта мы создаем и используем только один индивидуальный процессор. Собственно, вот именно этот процессор сможет каждый для себя использовать при импорте в минишоп из экселя. Все, что потребуется — изменить параметры шаблонов категорий и товаров, и корневого раздела каталога, а так же подправить поля-колонки.

Уточню: этот импортер рассчитан на два уровня вложенности: Корневой раздел -> Категория -> Товар. Для многоуровневой вложенности его придется чуть-чуть допилить.

Код процессора:
<?php

require_once dirname(dirname(dirname(dirname(dirname(__FILE__))))) . '/modimporter/import/xlsx/console.class.php';

class modModimporterCustomServersImportConsoleProcessor extends modModimporterImportXlsxConsoleProcessor{
    
    
    
    public function initialize(){
        
        $this->setDefaultProperties(array(
            "category_root_id"  => 4,        // Серверы
            "category_template_id"  => 5,        // Шаблон категории
            "good_template_id"  => 4,        // Шаблон товара
        ));
        
        return parent::initialize();
    }
    
    
    protected function StepWriteTmpCommercialInfo(){
        return $this->nextStep("modimporter_write_tmp_goods", "Начинаем разбор исходных данных", null, xPDO::LOG_LEVEL_WARN);
    }
    
    
    /*
        Записываем категории
    */
    protected function StepWriteTmpGoods(){
        
        
        if(!$reader = & $this->getReader()){
            return $this->failure('Не был получен ридер');
        }
        
        $filename = 'xl/worksheets/sheet1.xml';
        
        
        if(!$path = $this->getImportPath()){
            
            return $this->failure("Не была получена директория файлов");
        }
        
        
        $schema = array(
            "worksheet"    => array(
                "sheetData" => array(
                    "row"     => array(
                        "parse" => true,
                    ),
                ),
            ),
        );
        
        $index = 0;
        $skip = 1;  // Сколько строчек пропустить (техническая информация)
        
        
        $limit = $this->getProperty("limit", 0);
        $count = 0;
        
        if(!$inserted = (int)$this->getSessionValue("inserted")){
            $inserted = 0;
        }
        
        $next_step = false;
        
        
        $result = $reader->read(array(
            "file" => $path.$filename,
        ), function(modImporterXmlReader $reader) use (& $schema, & $index, & $skip, $limit, &$count, &$inserted, &$next_step){
            
            $xmlReader = & $reader->getReader();
            
            $node = $reader->getNodeName($xmlReader);            
            
            if(!$reader->isNodeText($xmlReader) && $reader->getSchemaNodeByKey($schema, $node) && $reader->isNode($node, $xmlReader)){                                
                
                if(isset($schema["parse"]) && $schema["parse"] && $node == 'row'){
                    
                    
                    if($index + 1 > $skip){
                        
                        // Счетчик прочтенных элементов
                        $count++;
                        
                        
                        // XML-массив строчки из экселя
                        $xml = $reader->getXMLNode($xmlReader);   
                        # print_r($xml);
                        
                        $attributes = $xml->attributes();
                        
                        $columns_data = array();        // Данные колонок
                        
                        foreach($xml->c as $column_index => $column){
                            
                            $column_real_index = $column_index + 1; // Для человеков
                            
                            $column_attributes = $column->attributes();
                            
                            $value = (string)$column->v;
                            
                            // Определяем данные колонки в таблице строковой находятся или в самом XML
                            if(isset($column_attributes['t']) AND $column_attributes['t'] == 's'){
                                
                                if(!$string_object = $this->getImportObject($value, "shared_string")){
                                    $error = "Не был получен строковый объект для записи. Строка '{$index}' колонка '{$column_real_index}'";
                                    
                                    $this->modx->log(xPDO::LOG_LEVEL_ERROR, $error, '', __CLASS__, __FILE__, __LINE__);
                                    
                                    return $error;
                                }
                                else{
                                    $value = $string_object->tmp_title;
                                }
                            }
                            
                            $columns_data[] = $value;
                        }
                        
                        // Если есть данные строчки и артикул, то записываем данные
                        if(
                            $columns_data 
                            AND !empty($columns_data[0]) 
                            AND $article = trim($columns_data[0])
                        ){
                            
                            $object = $this->createImportObject($article, array(
                                "tmp_title" => $article,        // Артикул
                                "tmp_parent" => $columns_data[1],       // Категория
                                "tmp_raw_data" => $columns_data,        // Все данные колонок строки
                            ), "product");
                            
                            
                            # print_r($object->toArray());
                            
                            if(!$object->save()){
                                $this->modx->log(2, "Ошибка сохранения записи товара. Строка '{$index}'");
                            }
                        }
                    }
                    
                    $index++;
                    
                    return true;
                }                               
                
            }           
            
            return true;
        });
        
        if($result !== true AND $result !== false){
            return $this->failure($result);
        }
        
        if($next_step){
            return $this->progress("Прочитано {$inserted} строк.", null, xPDO::LOG_LEVEL_DEBUG);
        }
        
        
        return $this->nextStep("modimporter_write_tmp_categories", "Товары успешно записаны");
    }
    
    
    /*
        Записываем категории
    */
    protected function StepWriteTmpCategories(){
        
        /*
            Категории получаем просто выборкой уникальных родителей товаров из временной таблицы
        */
        
        $q = $this->modx->newQuery("modImporterObject");
        $q->distinct();
        $q->select(array(
            "tmp_parent",
        ));
        $q->where(array(
            "tmp_object_type"   => "product",
        ));
        
        $s = $q->prepare();
        $s->execute();
        
        # print_r($s->errorInfo());
        
        while($row = $s->fetch(PDO::FETCH_ASSOC)){
            
            # print_r($row);
            
            $category = $row['tmp_parent'];
            
            $object = $this->createImportObject($category, array(
                "tmp_title" => $category,        // Артикул
                "tmp_parent" => null,       
            ), "category");
            
            
            # print_r($object->toArray());
            
            if(!$object->save()){
                return "Ошибка сохранения записи категории '{$row['tmp_parent']}'";
            }
        }
        
        return $this->nextStep("modimporter_import_data", "Категории успешно записаны");
    }
    
    
    /*
        Обновляем категории
    */
    protected function StepImportUpdateCategories(){
        
        /*
            Получаем только те временные данные, для которых есть имеющиеся категории
        */
        
        $category_root_id = $this->getProperty("category_root_id");
        $category_template_id = $this->getProperty("category_template_id");
        
        $q = $this->modx->newQuery("modImporterObject");
        $q->innerJoin("modResource", "category", "category.parent = {$category_root_id} AND category.template = {$category_template_id} AND category.pagetitle = modImporterObject.tmp_external_key");
        
        $q->where(array(
            "tmp_object_type" => "category",
            "tmp_processed" => 0,
        ));
        
        $q->limit(1);
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
        }
        
        return parent::StepImportUpdateCategories();
    }
    
    
    
    // Создаем категории
    protected function StepImportCreateCategories(){
        
        $category_root_id = $this->getProperty("category_root_id");
        $category_template_id = $this->getProperty("category_template_id");
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $q->where(array(
            "tmp_object_type" => "category",
            "tmp_processed" => 0,
        ));
        
        $q->limit(1);
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $pagetitle = $tmp_object->tmp_external_key;
            $alias = "test-{$pagetitle}";
            
            $data = array(
                "pagetitle"     => $pagetitle,
                "parent"        => $category_root_id,
                "alias"         => $alias,
                "template"      => $category_template_id,
                "class_key"     => 'msCategory',
                "published"     => 1,
                "isfolder"      => 1,
            );
            
            # print_r($data);
            # 
            # return;
            
            $response = $this->modx->runProcessor('resource/create', $data);
            
            if($response->isError()){
                $tmp_object->tmp_error = 1;
                $tmp_object->tmp_error_msg = json_encode($response->getResponse());
                $tmp_object->save();
                return $response->getResponse();
            }
            
            // else
            $object = $response->getObject();
            $tmp_object->tmp_resource_id = $object['id'];
            $tmp_object->save();
        }
        
        return parent::StepImportCreateCategories();
    }
    
    
    /*
        Обновляем товары
    */
    protected function StepImportUpdateGoods(){
        /*
            Получаем только те временные данные, для которых есть имеющиеся категории
        */
        
        $good_template_id = $this->getProperty('good_template_id');
        
        $category_root_id = $this->getProperty("category_root_id");
        $category_template_id = $this->getProperty("category_template_id");
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->innerJoin("msProductData", "product_data", "product_data.article = {$alias}.tmp_external_key");
        $q->innerJoin("msProduct", "product", "product.id = product_data.id");
        
        $q->where(array(
            "tmp_object_type" => "product",
            "tmp_processed" => 0,
        ));
        
        $columns = $this->modx->getSelectColumns("msProduct", $tableAlias= 'product', $columnPrefix= '', $columns= array ("id", "class_key"), $exclude= true);
        $columns = explode(", ", $columns);
        $q->select($columns);
        
        $columns = $this->modx->getSelectColumns("msProductData", $tableAlias= 'product_data', $columnPrefix= '', $columns= array ("id"), $exclude= true);
        $columns = explode(", ", $columns);
        $q->select($columns);
        
        $q->select(array(
            "{$alias}.*",
            "product.id as resource_id",
        ));
        
        $q->limit(1);
        
        $limit = $this->getProperty("limit", 50);
        
        if(!$processed = (int)$this->getSessionValue("goods_updated")){
            $processed = 0;
        }
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            $tmp_object->set('tmp_resource_id', $tmp_object->resource_id);
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            
            $data = $tmp_object->toArray();
            
            # print_r($data);
            
            $raw_data = $data['tmp_raw_data'];
            
            // Переопределяем значения обновляемых товаров
            $data = array_merge($data, array(
                "id"    => $data['resource_id'],
                "warranty" => $raw_data[4],
                "stock" => $raw_data[5],
                "price" => $raw_data[6],
            ));
            
            # print_r($data);
            # 
            # return;
            
            $response = $this->modx->runProcessor('resource/update', $data);
            
            if($response->isError()){
                $tmp_object->tmp_error = 1;
                $tmp_object->tmp_error_msg = json_encode($response->getResponse());
                
                $this->modx->log(xPDO::LOG_LEVEL_ERROR, print_r($response->getResponse(), 1), '', __CLASS__, __FILE__, __LINE__);
            }
            else{
                
                $object = $response->getObject();
                $tmp_object->tmp_resource_id = $object['id'];
            }
            
            $tmp_object->save();
            
            
            $processed++;
                
            if($limit AND $processed%$limit === 0){
                $this->setSessionValue("goods_updated", $processed);
                return $this->progress("Обновлено {$processed} товаров");
            }
        }
        
        return parent::StepImportUpdateGoods();
    }
    
    
    // Создаем товары
    protected function StepImportCreateGoods(){
        
        $good_template_id = $this->getProperty('good_template_id');
        
        $category_root_id = $this->getProperty("category_root_id");
        $category_template_id = $this->getProperty("category_template_id");
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->innerJoin("modResource", "category", "category.parent = {$category_root_id} AND category.template = {$category_template_id} AND category.pagetitle = modImporterObject.tmp_parent");
        
        $q->where(array(
            "tmp_object_type" => "product",
            "tmp_processed" => 0,
        ));
        
        $q->select(array(
            "{$alias}.*",
            "tmp_external_key as pagetitle",
            "tmp_title as longtitle",
            "category.id as parent",
        ));
        
        $q->limit(1);
        
        $limit = $this->getProperty("limit", 100);
        
        if(!$inserted = (int)$this->getSessionValue("goods_inserted")){
            $inserted = 0;
        }
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $raw_data = $tmp_object->get('tmp_raw_data');
            
            # print_r($raw_data);
            
            $article = $raw_data[0];
            $pagetitle = $raw_data[2];
            $alias = "{$pagetitle}-{$article}";
            
            $data = array(
                "class_key" => 'msProduct',
                "published" => 1,
                "isfolder"  => 0,
                "template"  => $good_template_id,
                "parent"    => $tmp_object->parent,
                "article" => $article,
                "pagetitle" => $pagetitle,
                "alias"     => $alias,
                "longtitle" => $raw_data[3],
                "warranty" => $raw_data[4],
                "stock" => $raw_data[5],
                "price" => $raw_data[6],
            );
            
            # $data = array_merge($tmp_object->toArray(), array(
            # ));
            # 
            # print_r($data);
            # 
            # return;
            
            $response = $this->modx->runProcessor('resource/create', $data);
            
            if($response->isError()){
                $tmp_object->tmp_error = 1;
                $tmp_object->tmp_error_msg = json_encode($response->getResponse());
                $tmp_object->save();
                return $response->getResponse();
            }
            
            $object = $response->getObject();
            $tmp_object->tmp_resource_id = $object['id'];
            $tmp_object->save();
            
            $inserted++;
            
            if($limit AND $inserted%$limit === 0){
                $this->setSessionValue("goods_inserted", $inserted);
                return $this->progress("Создано {$inserted} новых товаров");
            }
            
            # return false;
        }
        
        return parent::StepImportCreateGoods();
    }
    
    
}

return 'modModimporterCustomServersImportConsoleProcessor';
Предлагаю каждому самостоятельно почитать код, а возникающие вопросы писать в комменты. Так мы сосредоточимся на самом важном и не понятном.
Fi1osof
19 января 2016, 10:26
modx.pro
1
4 008
+5

Комментарии: 9

Антон Левиц
Антон Левиц
26 июля 2016, 17:29
1
0
При повторной загрузке xlsx файла с обновленными параметрами выдает ошибку

Что-то пошло не так... Попробуйте еще разок!
Товары успешно обновлены
Товары успешно сняты с публикации
Тестировал код без корректировок.

Товар не обновляется
    Лев Вербицкий
    29 июля 2016, 23:47
    +2
    Антон, добрый вечер. Ваш вопрос был решен?)
      Антон Левиц
      Антон Левиц
      30 июля 2016, 03:13
      +1
      Да, В техподдержке вы ответили на все вопросы и решили проблему. Еще раз вам спасибо!
    Максим Кузнецов
    22 мая 2017, 13:25
    0
    Подскажите, пожалуйста, текущая версия (1.6.3) — рабочая?

    Создаю свой расширяющий класс:
    <?php
    	require_once dirname(dirname(dirname(dirname(__FILE__)))).'/xlsx/console.class.php';
    
    	class modModimporterCustomImportMinishopConsoleProcessor extends modModimporterImportXlsxConsoleProcessor {
    		public function initialize(){
    			$this->setDefaultProperties(array(
    				"category_root_id"  => 1,
    				"category_template_id"  => (int) $this->modx->getOption('modimporter.category_template_id'),
    				"good_template_id"  => (int) $this->modx->getOption('modimporter.product_template_id'),
    			));
    			
    			return parent::initialize();
    		}
    
    
    		protected function StepWriteTmpCommercialInfo(){
    			return $this->nextStep("modimporter_write_tmp_goods", "Тест", null, xPDO::LOG_LEVEL_WARN);
    		}
    	}
    
    	return 'modModimporterCustomImportMinishopConsoleProcessor';

    И на выходе получаю ошибку:
    PHP warning: Declaration of modImporterReader::initialize(modProcessor &$processor) should be compatible with modProcessor::initialize()
      Fi1osof
      22 мая 2017, 13:49
      0
      Да, рабочая, но изначально там было нарушение рекларации, и сейчас просто так это не поправить (есть зависимый функционал).
      Позже переделаю, а пока скорее всего поможет в начале своего расширяющего процессора прописать ini_set(«display_errors», 0); или типа того, чтобы подавить ошибки.
        Максим Кузнецов
        19 июня 2017, 10:06
        0
        Можно поинтересоваться, для чего были закомментированы следующие строки?
        $filename = 'xl/sharedStrings.xml';
        
                # if(!$filename = $this->getProperty('filename')){
                #     return $this->failure("Не был указан файл");
                # }
        После раскомментирования, проблема с декларацией исчезла
        (правда, все равно ничего не импортирует, но это уже другой вопрос :) )
          Fi1osof
          19 июня 2017, 11:52
          0
          Данные строки прописаны конкретно для разбора XML-а из XLSX-файлов. То есть сначала эвселевский файл распаковывается как обычный архив, потом читается файл с данными (путь всегда один). Если вы именно этот механизм пытаетесь использовать для чего-то другого, скорее всего он не будет работать как надо.
          Максим Кузнецов
          19 июня 2017, 10:08
          0
          Теперь строчки выше возвращают:
          [2017-06-19 10:07:37] (ERROR @ /core/components/modimporter/model/modimporter/reader/modimporterxmlreader.class.php : 126) PHP warning: XMLReader::read(): PK
          [2017-06-19 10:07:37] (ERROR @ /core/components/modimporter/model/modimporter/reader/modimporterxmlreader.class.php : 126) PHP warning: XMLReader::read(): ^
            Fi1osof
            19 июня 2017, 11:53
            0
            Ничего кроме «Что-то пошло не так» я не могу сказать, для этого надо смотреть проект.
        Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
        9