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

Как я и писал в прошлом релизе, modImporter сейчас — это больше основа для настройки импорта, и конечно же что-то придется пилить самому. Но сегодня мы попробуем довольно детально рассмотреть процесс базовой настройки под minishop2. Конечно кому-то материал покажется сложным, но это будет своего рода тестом для вас — на сколько хорошо вы понимаете как устроен MODX. Если внимательно изучить представленный материал, то в целом импорт сможет наладить даже начинающий разработчик. Тут сразу же могу настоятельно посоветовать к изучению уроки по xPDO от Ильи Уткина, они многим здесь помогут в понимании происходящего. Так же сразу советую поставить консоль, если не стоит. Она очень сильно нам поможет в отладке.

Итак, у нас имеется интернет-магазин на minishop2 и вот такой вот CSV-файлик для примера. Попробуем настроить импорт.

Внимание!!! Не играйтесь на боевых сайтах. Делайте для этого дев-копии или лучше вообще чистые сайты. И не забываем про бекапы.

1. Устанавливаем пакет modImporter на сайт.
С этим все просто — покупаем его в modstore.pro и устанавливаем через менеджер пакетов.

Здесь только пара моментов:
— Настоятельно советую в системных настройках сразу указать источник файлов для импортера (он создается автоматически).



— По умолчанию длина первичных ключей — 32 символа. Если у вас артикулы будут длиннее, надо указать соответствующую длину в настройке modimporter.external_key_length (крейсерская длина до 255 символов). Но лучше стараться, чтобы артикулы были какой-то определенной длины.

2. Создаем собственный процессор-импортер.
Сегодня я выложил обновленную версию modImporter-1.1.0, и в ней добавлен CSV-ридер. Его нам и предстоит использовать.

Итак, в modImporter есть специальная папка processors/custom/



Туда мы и создаем свой новый процессор custom/import/minishop/console.class.php



Это и будет наш главный исполняющий файл.

Пропишем в него вот такой код (он большой, но публикую его сюда, чтобы детально разбирать можно было):
<?php

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

class modModimporterCustomImportMinishopConsoleProcessor extends modModimporterImportConsoleProcessor{
    
    
    public function initialize(){
        
        $this->setProperties(array(
            'readerClassname'   => 'reader.modImporterCsvReader',
            "skip_lines"        => 1,       // Сколько линий пропускать
            "category_tpl"      => 6,       // Шаблон категории
            "categories_parent"      => 7,  // Корневой раздел каталога
            "product_tpl"      => 7,        // Шаблон товара
        ));
        
        return parent::initialize();
    }
    
    
    protected function StepWriteTmpData(){
        
        if(!$reader = & $this->getReader()){
            return $this->failure('Не был получен ридер');
        }
        
        
        if(!$filename = $this->getProperty('filename')){
            return $this->failure("Не был указан файл");
        }
        
        
        if(!$path = $this->getImportPath()){
            
            return $this->failure("Не была получена директория файлов");
        }
        
        
        $skip_lines = (int)$this->getProperty('skip_lines');
        
        $count = 0;
        
        $categories = array();
        
        $result = $reader->read(array(
                "file" => $path.$filename,
            ), function(modImporterCsvReader $reader, $data) use (& $count, $skip_lines, & $categories){
                
                $count++;
                
                // Пропускаем строки, сколько надо (как правило одну, с заголовками)
                if($skip_lines && ($skip_lines >= $count)){
                    return true;
                }
                
                # print_r($data);
                
                
                /*
                    Получаем только уникальные категории
                */
                $category = $data[0];
                
                if(!isset($categories[$category])){
                    $categories[$category] = array(
                        "pagetitle" => $data[1],
                        "longtitle" => $data[2],
                    );
                }
                
                
                // Сохраняем запись товара
                $article = $data[3];
                $product = array(
                    "pagetitle" => $data[4],
                    "longtitle" => $data[5],
                    "price" => $data[6],
                    "weight" => $data[7],
                    "quantity" => $data[8],
                );
                $object = $this->createImportObject($article, array(
                    "tmp_title"  => $product['pagetitle'],
                    "tmp_parent"  => $data[0],
                    "tmp_raw_data"  => $product,
                ), "product");
                
                if(!$object->save()){
                    $this->modx->log(xPDO::LOG_LEVEL_ERROR, "Ошибка сохранения записи товара", '', __FUNCTION__, __FILE__, __LINE__);
                    $this->modx->log(xPDO::LOG_LEVEL_ERROR, print_r($object->toArray(), 1), '', __FUNCTION__, __FILE__, __LINE__);
                }
                
                # print_r($object->toArray());
                
                return true;
            }
        );
        
        if($result !== true AND $result !== false){
            return $this->failure($result);
        }
        
        # print_r($categories);
        
        /*
            Если категории имеются, сохраняем им
        */
        if($categories){
            foreach($categories as $category_article => $category){
                $object = $this->createImportObject($category_article, array(
                    "tmp_title"  => $category['pagetitle'],
                    "tmp_raw_data"  => $category,
                ), "category");
                
                if(!$object->save()){
                    return $this->failure("Ошибка сохранения записи категории");
                }
            }
        }
        
        return $this->nextStep("modimporter_write_tmp_commercial_info", "Начинаем разбор исходных данных", null, xPDO::LOG_LEVEL_WARN);
    }
    
    # protected function StepDropTmpTables(){
    #     return $this->nextStep("modimporter_create_tmp_tables", "Временные таблицы успешно удалены");
    # }
    
    protected function StepImportUpdateCategories(){
        
        /*
            Получаем только те временные данные, для которых есть имеющиеся категории
        */
        $q = $this->prepareGetCategoryQuery();
        
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            $tmp_object->tmp_resource_id = $tmp_object->category_id;
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $data = $tmp_object->toArray();
            
            
            // Подготавливаем конечные данные категории
            $data = $this->prepareCategoryUpdateData($data);
            
            $response = $this->modx->runProcessor('resource/update', $data);
            
            if($response->isError()){
                $tmp_object->tmp_error = 1;
                $tmp_object->tmp_error_msg = json_encode($response->getResponse());
                $tmp_object->save();
            }
        }
        
        return parent::StepImportUpdateCategories();
    }
    
    
    protected function prepareGetCategoryQuery(){
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->innerJoin("modResource", "category", "category.externalKey = {$alias}.tmp_external_key");
        
        $q->where(array(
            "tmp_object_type" => "category",
            "tmp_processed" => 0,
        ));
        
        $columns = $this->modx->getSelectColumns("modResource", "category", '', array('id', 'class_key'), true);
        $columns = explode(", ", $columns); 
        
        $q->select($columns);
        
        $q->select(array(
            "category.id as category_id",
            "category.class_key as resource_class_key",
            "{$alias}.*",
        ));
        
        $q->limit(1);
        
        return $q;
    }
    
    
    protected function prepareCategoryUpdateData(array $data){
        
        $data = array_merge($data, array(
            "id"     => $data['category_id'],       // Устанавливаем id документа
            "class_key"     => $data['resource_class_key'],     // Устанавливаем class_key документа
            "pagetitle"     => $data['tmp_raw_data']['pagetitle'],
            "longtitle"     => $data['tmp_raw_data']['longtitle'],
            "published"     => 1,
        ));
        
        return $data;
    }
    
    
    protected function StepImportCreateCategories(){
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->where(array(
            "tmp_object_type" => "category",
            "tmp_processed" => 0,
        ));
        
        $q->select(array(
            "{$alias}.*",     
            "{$alias}.tmp_external_key as externalKey",     // Артикул в документ
        ));
        
        $q->limit(1);
        
        $limit = $this->getProperty("limit", 100);
        
        if(!$processed = (int)$this->getSessionValue("category_processed")){
            $processed = 0;
        }
        
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            // Отмечаем временную запись как отработанную
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $data = $tmp_object->toArray();
            
            $data = $this->prepareCategoryCreateData($data);
            
            $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();
            
            # print_r($object);
            
            $processed++;
            
            if($limit AND $processed%$limit === 0){
                $this->setSessionValue("category_processed", $processed);
                return $this->progress("Создано {$processed} категорий");
            }
        }
        
        return parent::StepImportCreateCategories();
    }
    
    
    protected function prepareCategoryCreateData(array $data){
        
        $parent = $this->getProperty('categories_parent');
        $template = $this->getProperty('category_tpl');
        
        $data = array_merge($data, array(
            "parent"    => $parent,
            "template"    => $template,
            "class_key"     => 'msCategory',     // Устанавливаем class_key документа
            "pagetitle"     => $data['tmp_raw_data']['pagetitle'],
            "longtitle"     => $data['tmp_raw_data']['longtitle'],
            "isfolder"     => 1,
            "published"     => 1,
        ));
        
        return $data;
    }
    
    
    protected function StepImportUpdateGoods(){
        
        /*
            Получаем только те временные данные, для которых есть имеющиеся категории
        */
        $q = $this->prepareGetGoodQuery();
        
        $limit = $this->getProperty("limit", 100);
        
        if(!$processed = (int)$this->getSessionValue("goods_processed")){
            $processed = 0;
        }
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            $tmp_object->tmp_resource_id = $tmp_object->category_id;
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $data = $tmp_object->toArray();
            
            
            // Подготавливаем конечные данные категории
            $data = $this->prepareGoodUpdateData($data);

            $response = $this->modx->runProcessor('resource/update', $data);
            
            if($response->isError()){
                $tmp_object->tmp_error = 1;
                $tmp_object->tmp_error_msg = json_encode($response->getResponse());
                $tmp_object->save();
            }
            
            $processed++;
            
            if($limit AND $processed%$limit === 0){
                $this->setSessionValue("goods_processed", $processed);
                return $this->progress("Обновлено {$processed} товаров");
            }
        }
        
        
        return parent::StepImportUpdateGoods();
    }
    
    
    protected function prepareGetGoodQuery(){
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->innerJoin("modResource", "product", "product.externalKey = {$alias}.tmp_external_key");
        
        $q->where(array(
            "tmp_object_type" => "product",
            "tmp_processed" => 0,
        ));
        
        $columns = $this->modx->getSelectColumns("modResource", "product", '', array('id', 'class_key'), true);
        $columns = explode(", ", $columns); 
        
        $q->select($columns);
        
        $q->select(array(
            "product.id as product_id",
            "product.class_key as resource_class_key",
            "{$alias}.*",
        ));
        
        $q->limit(1);
        
        return $q;
    }
    
    
    protected function prepareGoodUpdateData(array $data){
        
        $data = array_merge($data, array(
            "id"     => $data['product_id'],       // Устанавливаем id документа
            "class_key"     => $data['resource_class_key'],     // Устанавливаем class_key документа
            "pagetitle"     => $data['tmp_raw_data']['pagetitle'],
            "longtitle"     => $data['tmp_raw_data']['longtitle'],
            "price"     => $data['tmp_raw_data']['price'],
            "weight"     => $data['tmp_raw_data']['weight'],
            "published"     => 1,
        ));
        
        // Получаем все текущие TV-шки
        // Нельзя на апдейт передать не все TVшки, так как остальные просто затрутся
        $tvs = array();
        
        $q = $this->modx->newQuery('modTemplateVarResource', array(
            "contentid"   => $data['id'],
        ));
        
        $alias = $q->getAlias();
        
        $q->select(array(
            "{$alias}.*",
        ));
        
        $s = $q->prepare();
        $s->execute();
        
        
        while($row = $s->fetch(PDO::FETCH_ASSOC)){
            $tvs["tv".$row['tmplvarid']]  = $row['value'];
        }
        
        
        $tvs = array_merge($tvs, array(
            "tv12"  => $data['tmp_raw_data']['quantity'],
        ));
        
        if($tvs){
            
            foreach($tvs as $tv_id => $tv){
                $data[$tv_id] = $tv;
            }
            
            $data["tvs"] = 1;
        }
        
        return $data;
    }
    
    
    protected function StepImportCreateGoods(){
        
        $q = $this->modx->newQuery("modImporterObject");
        
        $alias = $q->getAlias();
        
        $q->innerJoin("modImporterObject", "tmp_category", "tmp_category.tmp_object_type = 'category' AND tmp_category.tmp_external_key = {$alias}.tmp_parent");
        
        $q->where(array(
            "tmp_object_type" => "product",
            "tmp_processed" => 0,
        ));
        
        $q->select(array(
            "{$alias}.*",     
            "tmp_category.tmp_resource_id as parent",     // ID категории
            "{$alias}.tmp_external_key as externalKey",     // Артикул в документ
        ));
        
        $q->limit(1);
        
        $limit = $this->getProperty("limit", 100);
        
        if(!$processed = (int)$this->getSessionValue("goods_created")){
            $processed = 0;
        }
        
        
        while($tmp_object = $this->modx->getObject("modImporterObject", $q)){
            
            # print_r($tmp_object->toArray());
            # 
            # break;
            
            // Отмечаем временную запись как отработанную
            $tmp_object->tmp_processed = 1;
            $tmp_object->save();
            
            $data = $tmp_object->toArray();
            
            $data = $this->prepareGoodCreateData($data);
            
            # print_r($data);
            # break;
            
            $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();
            
            # print_r($object);
            # print_r($tmp_object->toArray());
            
            $processed++;
            
            if($limit AND $processed%$limit === 0){
                $this->setSessionValue("goods_created", $processed);
                return $this->progress("Создано {$processed} товаров");
            }
            
            # break;
        }
        
        return parent::StepImportCreateGoods();
    }
    
    
    protected function prepareGoodCreateData(array $data){
        
        $template = $this->getProperty('product_tpl');
    
        $data = array_merge($data, array(
            "template"    => $template,
            "class_key"     => 'msProduct',     // Устанавливаем class_key документа
            "pagetitle"     => $data['tmp_raw_data']['pagetitle'],
            "longtitle"     => $data['tmp_raw_data']['longtitle'],
            "price"     => $data['tmp_raw_data']['price'],
            "weight"     => $data['tmp_raw_data']['weight'],
            "tv12"     => $data['tmp_raw_data']['quantity'],
            "isfolder"     => 0,
            "published"     => 1,
        ));
        
        return $data;
    }
    
}

return 'modModimporterCustomImportMinishopConsoleProcessor';

Забегая вперед, обращу ваше внимание на то, что id-шники шаблонов и т.п., прописанные в методе initialize() (и TVшки всякие в методах работы с документами), у вас конечно же буду свои.

Прежде чем разбирать детально код, проверим сначала, что он работает. Для этого зайдем в консоль и выберем скрипт выполнения импортера (он устанавливается теперь вместе с modImporter).



Вот его полный код, чтобы наглядней было:
<?php 
print '<pre>';
ini_set('display_errors', 1);
$modx->switchContext('web');
$modx->setLogLevel(3);
$modx->setLogTarget('HTML');
 
$namespace = 'modimporter';        // Неймспейс комопонента


// Добавить веб-сессию текущего пользователя
// Если вы не авторизованы во фронте, можете получить 
// в результате выполнения сообщение Доступ запрещён!
// $modx->user->addSessionContext('web');

// Удалить веб-сессию текущего пользователя
// $modx->user->removeSessionContext('web');


// Сбросить сессию компонента
// $_SESSION["SM_1C_IMPORT"] = array();

// Вывести данные сессии компонента
// print_r($_SESSION["SM_1C_IMPORT"]);

// print_r($_SESSION["SM_1C_IMPORT"]);
// unset($_SESSION["modImporter"]);


$params = array(  
    // "step"  => "SDFsdf",
    "debug" => false,
    // "mode" => "checkauth",
    // "modimporter_step" => "modimporter_checkauth",
    // "modimporter_step" => "modimporter_console_init",
    // "modimporter_step" => "modimporter_drop_tmp_tables",
    // "modimporter_step" => "modimporter_create_tmp_tables",
    // "modimporter_step" => "modimporter_write_tmp_xlsx_shared_strings",
    // "modimporter_step" => "modimporter_write_tmp_categories",
    // "modimporter_step" => "modimporter_import_data",
    // "modimporter_step" => "modimporter_import_update_categories",
    // "modimporter_step" => "modimporter_import_update_categories",
    // "modimporter_step" => "modimporter_import_create_categories",
    // "modimporter_step" => "modimporter_import_update_goods",
    // "modimporter_step" => "modimporter_import_create_goods",
    // "modimporter_step" => "modimporter_import_flush_prices",
    // "modimporter_step" => "modimporter_import_create_prices",
    
    // "filename"  => "import.xml",
    // "username"  => "admin", 
    // "password"  => "wefwef", 
    "outputCharset" => "utf-8",
);

// $_SERVER['PHP_AUTH_USER'] = 'admin';
// $_SERVER['PHP_AUTH_PW'] = 'admin';

// if(!$response = $modx->runProcessor('modimporter/import/console',
if(!$response = $modx->runProcessor('modimporter/custom/import/minishop/console',
$params
, array(
'processors_path' => $modx->getObject('modNamespace', $namespace)->getCorePath().'processors/',
))){
print "Не удалось выполнить процессор";
return;
}

 
$memory = round(memory_get_usage(true)/1024/1024, 4).' Mb';
print "<div>Memory: {$memory}</div>";
$totalTime= (microtime(true) - $modx->startTime);
$queryTime= $modx->queryTime;
$queryTime= sprintf("%2.4f s", $queryTime);
$queries= isset ($modx->executedQueries) ? $modx->executedQueries : 0;
$totalTime= sprintf("%2.4f s", $totalTime);
$phpTime= $totalTime - $queryTime;
$phpTime= sprintf("%2.4f s", $phpTime);
print "<div>TotalTime: {$totalTime}</div>";

print_r($response->getResponse());

// $objects = $response->getObject();
// foreach($objects as $object){
// }

В этом скрипте изменим вызываемый процессор modimporter/import/console на наш modimporter/custom/import/minishop/console



При выполнении этого файла вы должны получить примерно вот такой ответ:
Memory: 7.75 Mb
TotalTime: 0.1500 s
Array
(
    [success] => 
    [message] => permission_denied
    [level] => 1
    [continue] => 
    [step] => 
    [data] => Array
        (
        )

    [object] => 
)

Иль вот такой:
Memory: 7.75 Mb
TotalTime: 0.0549 s
Array
(
    [success] => 
    [message] => Не указано действие
    [level] => 1
    [continue] => 
    [step] => 
    [data] => Array
        (
        )

    [object] => 
)

В любом случае, форма ответа должен быть схожим. Если вы получили что-то типа такого:
[2016-01-15 06:56:16] (ERROR @ /manager/components/console/connectors/console.php)
Processor /var/www/core/components/modimporter/processors/modimporter/custom/import/console.php does not exist; Array
(
    [processors_path] => /var/www/core/components/modimporter/processors/
)
Не удалось выполнить процессор
то скорее всего вы не в том месте создали процессор или не тот вызов прописали. Надеюсь ни у кого не возникнет этой досадной неприятности.

Будем считать, что у нас процессор выполняется. Чуть подробней рассмотрим представленные выше ответы.

Случай первый, когда мы получили ответ «permission_denied» (или «доступ запрещен, у кого какой язык включен в админке»). Это значит, что вы не авторизованы во фронте (в контексте web). Дело в том, что когда вы авторизовываетесь в админке, вы авторизовываетесь только в контексте mgr (конечно же это по умолчанию). А может и просто во фронте разлогинились. Так или иначе, вас во фронте нет и нашу сессию надо добавить в контекст web. Для этого в скрипте раскомментируем строчку $modx->user->addSessionContext('web'); и опять выполним скрипт. Получим типа такого сообщения:



Все, мы есть в контексте web и у нас есть права на выполнение импортера. Не указано только действие. А действий имеется не мало и основные из них мы и рассмотрим (особенно те, которые используются в нашем расширяющем процессоре). Вот чтобы выполнить какое-либо действие в самом импортере, нам надо раскомментировать соотвествующую строчку в параметрах. Например, modimporter_console_init.



Что мы здесь видим?
[success] => 1 — сигнализирует, что запрос выполнился успешно.
[message] => Консоль успешно инициализирована — просто пользовательское сообщение.
[continue] => 1 — сигнализирует, что надо повторно отправить запрос на сервер.
[step] => modimporter_import — указывает к какому шагу надо перейти.

Эти все параметры больше для той аджаксовой консоли, про которую я писал в прошлом топике, но нам это важно для отладки, чтобы мы понимали что вообще происходит и куда дальше идти.

Как вообще в процессоре отрабатываются эти шаги? Вот часть кода базового процессора modModimporterImportConsoleProcessor:
protected function processRequest(){ 
        
        if(!$step = trim($this->getProperty('modimporter_step'))){
            return $this->failure("Не указано действие");
        }
        
        switch($step){
                
            
            // Проверка авторизации
            case 'modimporter_checkauth':
                
                return $this->StepCheckouth();
                break;
            
            
            // Инициализация консоли
            case 'modimporter_console_init':
                
                return $this->StepInitConsole();
                break;
            
            // Загрузить файл
            case 'modimporter_upload_file':
                
                return $this->StepSaveFile();
                break;
            
            
            // Распаковать файл
            case 'modimporter_unzip_file':
                
                return $this->StepUnzipFile();
                break;

То есть, указывая тот или иной шаг, в процессоре вызывается указанный для него метод. Этих методов довольно много и конечно же обладателям компонента имеет смысл подсматривать полный их список. Какие-то из них имеют боле менее оконченную логику, например метод StepCheckouth
// Авторизация
protected function StepCheckouth(){
    
    $this->setDefaultProperties(array(
        "username"  => !empty($_SERVER['PHP_AUTH_USER']) ? $_SERVER['PHP_AUTH_USER'] : '',
        "password"  => !empty($_SERVER['PHP_AUTH_PW']) ? $_SERVER['PHP_AUTH_PW'] : '',
    ));
    
    if(
        !$this->modx->user->id
        OR !$this->modx->user->isAuthenticated($this->modx->context->key)
    ){
        
        $username = $this->getProperty('username');
        $password = $this->getProperty('password');
        
        if(!$username){
            return $this->failure('Не указан логин');
        }
        
        if(!$password){
            return $this->failure('Не указан пароль');
        }
        
        if (!$response = $this->modx->runProcessor('security/login', array(
            "username" => $username,
            "password" => $password,
        ))){
            return $this->failure("Ошибка выполнения запроса");
        }
        
        // else
        if ($response->isError()){
            if (!$msg = $response->getMessage()) 
            {
                $msg = "Ошибка авторизации.";
            }
            return $this->failure($msg);
        }
    }
    
    // else
    return $this->prepareAuthResponse();
}

То есть, если вы хотите проверить авторизацию произвольного пользователя этим методом, вы можете указать его и логин/пасс.


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

Здесь еще могут быть часто использованы шаги modimporter_drop_tmp_tables и modimporter_create_tmp_tables (соответственно удаляющие и создающие временные таблицы). Аджаксовая консоль по умолчанию проходит эти шаги каждый раз при выполнении, но вы и сами можете произвольно вызывать их во время отладки.

Ну а теперь рассмотрим по порядку непосредственно те методы-шаги, которые прописаны в нашем новом процессоре-импортере.

Метод StepWriteTmpData. Здесь процессор построчно читает указанный файл и сохраняет полученные данные во временную таблицу. Наверное, самое сложное здесь — это использование замыканий.
$result = $reader->read(array(
        "file" => $path.$filename,
    ), function(modImporterCsvReader $reader, $data) use (& $count, $skip_lines, & $categories){
        .........
	// Здесь массив данных из прочтенной строки
        return true;
    }
);
Здесь вызывается метод чтения нашего CSV-ридера

Код самого ридера совсем не большой:
<?php

require_once dirname(__FILE__) . '/modimporterreader.class.php';

class modImporterCsvReader extends modImporterReader{
    
    
    public function initialize(modProcessor & $processor){
        
        $this->setDefaultProperties(array(
            'csv_delimiter' => ",",
            'csv_enclosure' => '"',
            'csv_escape' => '\\',
        ));
        
        return parent::initialize($processor);
    }
    
    
    public function read(array $provider, $callback = null){
        
        if(empty($provider['file'])){
            return "Не был указан файл";
        }
        
        $file = $provider['file'];
        
        if(!$fo = fopen($file, 'r')){
            return $this->failure('Ошибка чтения файла');
        }
        
        $csv_delimiter = $this->getProperty('csv_delimiter');
        $csv_enclosure = $this->getProperty('csv_enclosure');
        $csv_escape = $this->getProperty('csv_escape');
        
        /**/
        while($data = fgetcsv ( $fo, 0, $csv_delimiter, $csv_enclosure, $csv_escape)){
            if(is_callable($callback)){
                $ok = $callback($this, $data);
                if($ok !== true){
                    return $ok;
                }
            }
        }
        return true;
    }
}

Все, что здесь имеет знать, что внутри нашей анонимной функции есть массив полученных данных $data и возможность работать с ним, в том числе с использованием текущим объектом процессора $this. В частности, мы здесь выполняем сразу две задачи:
1. Набиваем массив данных категорий $categories (и ниже уже эти данные в цикле сохраняем в БД).
2. Получаем данные товаров и так же сохраняем их в БД.

Здесь так же имеет смысл отдельно рассмотреть код создания объекта для временной таблицы из базового процессора.
protected function createImportObject($id, array $data = array(), $objectType = "", $className = "modImporterObject"){
    $object = $this->modx->newObject($className, $data);
    
    $object->set("tmp_external_key", $id);
    $object->set("tmp_object_type", $objectType);
    
    return $object;
}

И с ним же и метод получения объекта.
protected function getImportObject($id, $objectType = null, $className = "modImporterObject"){
    $condition = array(
        "tmp_external_key"  => $id,
    );
    
    if($objectType !== null){
        $condition["tmp_object_type"] = $objectType;
    }
    
    return $this->modx->getObject($className, $condition);
}

На самом деле все это просто синтаксический сахар и можно спокойно использовать методы xPDO::newObject()/xPDO::getObject(), но здесь я просто хотел отметить обязательные поля.

Еще раз уточню: это еще пока только сырые данные. Непосредственно обновлять/создавать категории/товары мы будет в других методах.

Итак, переходим к следующему методу StepImportUpdateCategories.
Здесь мы формируем запрос на получение документа категории (связка ВременныйОбъект-ДокументКатегории по ключу), и проходясь по каждой такой категории (получая их данные), выполняем ее обновление через системный процессор resource/update. Только обязательно внимательней изучите методы prepareGetCategoryQuery (подготовка объекта запроса к БД) и prepareCategoryUpdateData (подготовка данных на обновление категории). Здесь очень важно обратить внимание на формирование полей id и class_key. На уровне запроса мы не можем в чистом виде получать id категории, так как мы в этот момент получаем совершенно другой объект — объект временных данных, который после этого отмечаем как processed и сохраняем. А class_key не можем в чистом виде получать, так как xPDO, получив данные из БД, попытается создать инстанс объекта именно по этому class_key, то есть modResource/msCategory, а не modImporterObject.

Следом за обновлением категорий выполняется создание категорий StepImportCreateCategories (это с теми записями, для которых категории не нашлись и они processed => 0).
Здесь примерно все то же самое, что и с механизмом обновления категорий, только выполняется процессор resource/create.

В обоих этих методах, если категория была найдена, или была создана новая, временной записи устанавливается значение tmp_resource_id, это чтобы в дальнейшем при создании нового товара было легче получить id родителя.

Собственно, здесь я все довольно подробно изложил, так что я не буду отдельно еще рассматривать методы обновления и создания товаров, потому что там механизм сильно схож с категориями. Единственно, только на поля TVшек внимательней гляньте.

Ну а теперь подключим это все к аджаксовой консоли. Только для того, чтобы посмотреть как вообще она должна выполняться, запустим ее просто на главной страничке компонента.



Здесь можно не выбирать файл, а сразу запустить выполнение.



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

И вот теперь мы должны сделать так, чтобы консоль обращалась к нашему процессору. Для этого нам надо создать контроллер и пункт меню для него.

Начнем с контроллера.
1. В адресной строке на странице текущей аджакс-консоли изменим ?a=controllers/mgr/import/index&namespace=modimporter на ?a=controllers/custom/import/minishop/index&namespace=modimporter и перей по этому адресу. Вы должны увидеть вот такое:



Это MODX пытается найти запрошенный контроллер, но не находит его. Создадим его в папке custom-контроллеров. Только обратите внимание, что MODX пишет index.php, а надо файл создать index.class.php



Теперь, если мы обновим страницу, и если все хорошо, вы увидите или просто белый экран (если вывод ошибок в настройках сервера отключен), или сообщение типа такого:



Вот если все так, то осталось совсем чуть-чуть. Это значит, что файл там где надо и MODX его находит, просто надо прописать код контроллера. Вот его код:
<?php

require_once MODX_CORE_PATH . 'components/modimporter/controllers/mgr/import/index.class.php';

class ModimporterControllersCustomImportMinishopIndexManagerController extends ModimporterControllersMgrImportIndexManagerController{
    
    protected function getAction(){
        
        return 'custom/import/minishop/console';
    }
}

Все, если все ОК, то вы увидите такую же страницу с аджакс-импортером, как и на старой странице. Только теперь, если не указать файл и запустить импорт, мы должны увидеть сообщение об отсутствии файла.



Зальем туда наш CSV-файл и попробуем выполнить. Правда по умолчанию MODX не хочет принимать CSV и я получил такое сообщение:



Но это легко лечится: правим системную настройку, добавив расширение csv и обновляем страничку.



Все, теперь импорт выполняется успешно.



У меня категории и товары успешно прогрузились.



При чем прогрузились не только допсвойства товаров, но и TV-поля.


Ну и напоследок добавим пункт меню на наш новый контроллер. Для этого перейдем в редактор меню



и сделаем копию какого-нибудь пункта мен импортера, подправив контроллер.



Все поля обязательные.

Вот и собственно все, у нас новый импортер.



У кого какие вопросы возникают, спрашивайте.

P. S. Сорри а битые картинки. Я замахался и поправлять. Одни правишь, другие слетают. При этом они как бы все есть, потому что нельзя опять загрузить такой же файл, и самбы все имеются в редакторе. К топику они сейчас все идут как сопутствующие файлы. Надеюсь Василий поправит их, как в прошлый раз.
Fi1osof
15 января 2016, 05:58
modx.pro
5
8 136
+3

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

Василий Наумкин
15 января 2016, 09:34
0
— Настоятельно советую в системных настройках сразу указать источник файлов для импортера (он создается автоматически).
Это можно делать автоматически, при установке. Например, вот так.

Ну и хотелось бы видеть эту документацию на docs.modx.pro, там проще найти.

Надеюсь Василий поправит их, как в прошлый раз.
Поправил. Расскажи еще на досуге, как ты это делаешь? Я не смог этот баг нормально отловить в своё время — не получалось повторить на тесте.

Если что — вот здесь и здесь идёт замена ссылок при сохранении тикета.
    Fi1osof
    15 января 2016, 09:55
    0
    Это можно делать автоматически, при установке. Например, вот так.
    Да и у меня тоже подобных кейсов хватает, например здесь. Просто всех мелочей не успел еще прописать, пока просто предупредил.
    Тем не менее конечно подобные замечания наверняка много кому интересны будут.

    Ну и хотелось бы видеть эту документацию на docs.modx.pro, там проще найти.
    Это сложно назвать документацией. Пока это предварительное описание того, что есть сейчас, и все это требует обсуждения. В процессе пакет еще пилить и пилить, прежде чем будет какая-то устоявшаяся версия, которую уже можно будет документировать. Тогда и конечно же документация будет.

    Поправил. Расскажи еще на досуге, как ты это делаешь? Я не смог этот баг нормально отловить в своё время — не получалось повторить на тесте.
    Да ничего такого не делаю. Алгоритм такой: пишу топик, по ходу сохраняю, так же по ходу заливаю картинки в самом же редакторе (внизу), иногда нажимаю предпросмотр, и в процессе все эти шаги миксуются. Заметил, что даже без обновления страницы, картинки начинают биться. Что интересно, удаляешь битый блок, тут же нажимаешь вставить с самба внизу, и этот же блок работает (я правда не сводил отличаются там пути или нет). Я так поправил несколько картинок, а потом пока писал, опять куча побилась.
    Марк Валерич
    15 января 2016, 14:47
    +10
    Николай, приветствую Вас.
    Я как увидел описанный процесс настройки-испугался, что что-то у меня может не получиться.
    Вдруг руки не оттуда растут.
    А можно ли сделать все это так, чтобы я нажал на ссылочку, положил туда файлик или указал путь для xml, например. И этот компонент все сам сделал.
    Стоит он 1500 рублей, а шаманства еще на 5000.

    ПС.
    Отношусь к Вам с большим уважением.
    Но как раз вот этот шаманизм в процессе работы с Вашими компонентами, меня реально пугает.
    Из-за этого не стал рассматривать Shopmodx.

    ППС
    Пишу это потому, что была у Вас с Василием один раз перепалка как раз по этому поводу на много постов.
    Прошу не обижаться и воспринять эту хотелку Вашего будущего клиента.

      Павел Карелин
      15 января 2016, 15:02
      +5
      Я как быдлокодер испугался. И убежал в слезах. Это какой то лего конструктор получается, собери компонент себе сам))) Видимо я еще не дорос.
        Fi1osof
        15 января 2016, 15:22
        +2
        Марк, я предполагал такую реакцию. Тем не менее, я постарался максимально подробно все расписать, причем выбрал именно более сложный сценарий. Да, конечно же не всем этот компонент подойдет, тем более, что он устанавливает определенные требования к пониманию MODX на низком уровне. Но я уверен, что со временем выявится группа разработчиков, способных в этом всем разобраться. На самом деле там не все так страшно, как кажется. Кто с ООП знаком, вряд ли сильно испугается. Ну а вопросы по существу всегда можно задавать, я подскажу.

        Мысли на счет упрощения есть, и упрощения эти будут. Но именно эта статья будем полезна в дальнейшем для объяснения работы всего механизма. А так, кто-то готов месяц подождать, когда более юзерфрендли версия выйдет, а у кого-то уже проект горит, и знаний хватит под себя допилить, не такой уже и большой объем кода требуется под это.
      Alexander V
      16 января 2016, 08:42
      0
      А разве ImportX не справляется с задачей импорта, в чём разница? Товары Minishop2, это же обычные ресурсы.
      P.S. Еще один момент, это галерея изображений. Как быть с ней?
        Fi1osof
        16 января 2016, 19:02
        +1
        ImportX я особо не ковырял, но первое же, что я решил проверить, подтвердилось. Он использует стандартную MODX Console. В своем старом топике я писал
        Отмечу только, что это дело очень похоже на стандартный компонент MODx.Console, но это не он. Нативный компонент я попробовал, но отмел из-за того, что он только асинхронные запросы отправляет, не дожидаясь ответа. В общем, морду писал сам.
        Уточню в чем проблема: тот механизм рассчитан только на один запрос к серверу. Куча остальных запросов идут только на чтение статусов выполнения. И если по таймауту 30 секунд ваш запрос завершится, то импорт встанет колом. В моем решении каждый запрос — это отдельный запрос, так что можно прогружать десятки тысяч товаров, даже если это займет час-два.

        P.S. Еще один момент, это галерея изображений. Как быть с ней?
        Если говорить о минишопе, то можно так же, как и в импортере от Василия.
        Игорь Ткачук
        21 января 2016, 21:10
        0
        Купил ваш компонент. Будет подобная инструкция с xml?
          Fi1osof
          21 января 2016, 21:16
          0
          Смотрите вот этот топик. Там XLSX, но по сути это тот же самый XML-разбор. Отличие только в том, что перед разбором идет распаковка эксель-файла как архива, а у вас это будет без распаковки сразу чтение из XML.
          В любом случае, возникнут вопросы — обращайтесь по существу. Ну и по мере появления у меня очередных кейсов, я так же их буду освещать.
          Марк Валерич
          26 января 2016, 18:07
          0
          Николай, вопрос, что-то понять не могу.
          Мне заказчик говорит, пришли мне свое дерево с сайта, я его сопоставлю с деревом в 1с и с вэпэрю итоговый файл.
          Зашел msynk — там какая-то околесица, файл дурной вышел.

          Проект требует синхронизацию с 1с — и для начала простую выгрузку из minishopa в excel
          Вот эту задачу он сможет решить?
          Только так, чтобы было видно номер товара в дереве, номер родителя этого товара и поле price
            Fi1osof
            26 января 2016, 19:13
            0
            Вот я потому и не стал пока в этот пакет включать выгрузку, потому что пока вообще не понимаю в чем сложности выгрузки. Это же вообще делается довольно просто. Можете для примера посмотреть как сайтмэп формируется в NewsModxBox.
            Можете сформировать более четкие требования к вашей выгрузке, написать мне, я вам завтра ее напишу. Сегодня просто занят буду.
            Сергей Малышев
            25 августа 2016, 10:59
            0
            Продавать компонент, который нужно допиливать как-то некошерно…
              Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.
              13