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
Это и будет наш главный исполняющий файл.
Пропишем в него вот такой код (он большой, но публикую его сюда, чтобы детально разбирать можно было):
Забегая вперед, обращу ваше внимание на то, что id-шники шаблонов и т.п., прописанные в методе initialize() (и TVшки всякие в методах работы с документами), у вас конечно же буду свои.
Прежде чем разбирать детально код, проверим сначала, что он работает. Для этого зайдем в консоль и выберем скрипт выполнения импортера (он устанавливается теперь вместе с modImporter).
Вот его полный код, чтобы наглядней было:
В этом скрипте изменим вызываемый процессор modimporter/import/console на наш modimporter/custom/import/minishop/console
При выполнении этого файла вы должны получить примерно вот такой ответ:
Иль вот такой:
В любом случае, форма ответа должен быть схожим. Если вы получили что-то типа такого:
Будем считать, что у нас процессор выполняется. Чуть подробней рассмотрим представленные выше ответы.
Случай первый, когда мы получили ответ «permission_denied» (или «доступ запрещен, у кого какой язык включен в админке»). Это значит, что вы не авторизованы во фронте (в контексте web). Дело в том, что когда вы авторизовываетесь в админке, вы авторизовываетесь только в контексте mgr (конечно же это по умолчанию). А может и просто во фронте разлогинились. Так или иначе, вас во фронте нет и нашу сессию надо добавить в контекст web. Для этого в скрипте раскомментируем строчку $modx->user->addSessionContext('web'); и опять выполним скрипт. Получим типа такого сообщения:
Все, мы есть в контексте web и у нас есть права на выполнение импортера. Не указано только действие. А действий имеется не мало и основные из них мы и рассмотрим (особенно те, которые используются в нашем расширяющем процессоре). Вот чтобы выполнить какое-либо действие в самом импортере, нам надо раскомментировать соотвествующую строчку в параметрах. Например, modimporter_console_init.
Что мы здесь видим?
[success] => 1 — сигнализирует, что запрос выполнился успешно.
[message] => Консоль успешно инициализирована — просто пользовательское сообщение.
[continue] => 1 — сигнализирует, что надо повторно отправить запрос на сервер.
[step] => modimporter_import — указывает к какому шагу надо перейти.
Эти все параметры больше для той аджаксовой консоли, про которую я писал в прошлом топике, но нам это важно для отладки, чтобы мы понимали что вообще происходит и куда дальше идти.
Как вообще в процессоре отрабатываются эти шаги? Вот часть кода базового процессора modModimporterImportConsoleProcessor:
То есть, указывая тот или иной шаг, в процессоре вызывается указанный для него метод. Этих методов довольно много и конечно же обладателям компонента имеет смысл подсматривать полный их список. Какие-то из них имеют боле менее оконченную логику, например метод StepCheckouth
То есть, если вы хотите проверить авторизацию произвольного пользователя этим методом, вы можете указать его и логин/пасс.
Все, пользователь авторизован. Можно переходить к следующему шагу и он уже будет выполняться от имени данного пользователя (это особенно полезно, если вы хотите настроить какие-то ограниченные права для специального пользователя под импорт).
Здесь еще могут быть часто использованы шаги modimporter_drop_tmp_tables и modimporter_create_tmp_tables (соответственно удаляющие и создающие временные таблицы). Аджаксовая консоль по умолчанию проходит эти шаги каждый раз при выполнении, но вы и сами можете произвольно вызывать их во время отладки.
Ну а теперь рассмотрим по порядку непосредственно те методы-шаги, которые прописаны в нашем новом процессоре-импортере.
Метод StepWriteTmpData. Здесь процессор построчно читает указанный файл и сохраняет полученные данные во временную таблицу. Наверное, самое сложное здесь — это использование замыканий.
Код самого ридера совсем не большой:
Все, что здесь имеет знать, что внутри нашей анонимной функции есть массив полученных данных $data и возможность работать с ним, в том числе с использованием текущим объектом процессора $this. В частности, мы здесь выполняем сразу две задачи:
1. Набиваем массив данных категорий $categories (и ниже уже эти данные в цикле сохраняем в БД).
2. Получаем данные товаров и так же сохраняем их в БД.
Здесь так же имеет смысл отдельно рассмотреть код создания объекта для временной таблицы из базового процессора.
И с ним же и метод получения объекта.
На самом деле все это просто синтаксический сахар и можно спокойно использовать методы 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 его находит, просто надо прописать код контроллера. Вот его код:
Все, если все ОК, то вы увидите такую же страницу с аджакс-импортером, как и на старой странице. Только теперь, если не указать файл и запустить импорт, мы должны увидеть сообщение об отсутствии файла.
Зальем туда наш CSV-файл и попробуем выполнить. Правда по умолчанию MODX не хочет принимать CSV и я получил такое сообщение:
Но это легко лечится: правим системную настройку, добавив расширение csv и обновляем страничку.
Все, теперь импорт выполняется успешно.
У меня категории и товары успешно прогрузились.
При чем прогрузились не только допсвойства товаров, но и TV-поля.
Ну и напоследок добавим пункт меню на наш новый контроллер. Для этого перейдем в редактор меню
и сделаем копию какого-нибудь пункта мен импортера, подправив контроллер.
Все поля обязательные.
Вот и собственно все, у нас новый импортер.
У кого какие вопросы возникают, спрашивайте.
P. S. Сорри а битые картинки. Я замахался и поправлять. Одни правишь, другие слетают. При этом они как бы все есть, потому что нельзя опять загрузить такой же файл, и самбы все имеются в редакторе. К топику они сейчас все идут как сопутствующие файлы. Надеюсь Василий поправит их, как в прошлый раз.
Итак, у нас имеется интернет-магазин на 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. Сорри а битые картинки. Я замахался и поправлять. Одни правишь, другие слетают. При этом они как бы все есть, потому что нельзя опять загрузить такой же файл, и самбы все имеются в редакторе. К топику они сейчас все идут как сопутствующие файлы. Надеюсь Василий поправит их, как в прошлый раз.
Комментарии: 13
— Настоятельно советую в системных настройках сразу указать источник файлов для импортера (он создается автоматически).Это можно делать автоматически, при установке. Например, вот так.
Ну и хотелось бы видеть эту документацию на docs.modx.pro, там проще найти.
Надеюсь Василий поправит их, как в прошлый раз.Поправил. Расскажи еще на досуге, как ты это делаешь? Я не смог этот баг нормально отловить в своё время — не получалось повторить на тесте.
Если что — вот здесь и здесь идёт замена ссылок при сохранении тикета.
Это можно делать автоматически, при установке. Например, вот так.Да и у меня тоже подобных кейсов хватает, например здесь. Просто всех мелочей не успел еще прописать, пока просто предупредил.
Тем не менее конечно подобные замечания наверняка много кому интересны будут.
Ну и хотелось бы видеть эту документацию на docs.modx.pro, там проще найти.Это сложно назвать документацией. Пока это предварительное описание того, что есть сейчас, и все это требует обсуждения. В процессе пакет еще пилить и пилить, прежде чем будет какая-то устоявшаяся версия, которую уже можно будет документировать. Тогда и конечно же документация будет.
Поправил. Расскажи еще на досуге, как ты это делаешь? Я не смог этот баг нормально отловить в своё время — не получалось повторить на тесте.Да ничего такого не делаю. Алгоритм такой: пишу топик, по ходу сохраняю, так же по ходу заливаю картинки в самом же редакторе (внизу), иногда нажимаю предпросмотр, и в процессе все эти шаги миксуются. Заметил, что даже без обновления страницы, картинки начинают биться. Что интересно, удаляешь битый блок, тут же нажимаешь вставить с самба внизу, и этот же блок работает (я правда не сводил отличаются там пути или нет). Я так поправил несколько картинок, а потом пока писал, опять куча побилась.
Николай, приветствую Вас.
Я как увидел описанный процесс настройки-испугался, что что-то у меня может не получиться.
Вдруг руки не оттуда растут.
А можно ли сделать все это так, чтобы я нажал на ссылочку, положил туда файлик или указал путь для xml, например. И этот компонент все сам сделал.
Стоит он 1500 рублей, а шаманства еще на 5000.
ПС.
Отношусь к Вам с большим уважением.
Но как раз вот этот шаманизм в процессе работы с Вашими компонентами, меня реально пугает.
Из-за этого не стал рассматривать Shopmodx.
ППС
Пишу это потому, что была у Вас с Василием один раз перепалка как раз по этому поводу на много постов.
Прошу не обижаться и воспринять эту хотелку Вашего будущего клиента.
Я как увидел описанный процесс настройки-испугался, что что-то у меня может не получиться.
Вдруг руки не оттуда растут.
А можно ли сделать все это так, чтобы я нажал на ссылочку, положил туда файлик или указал путь для xml, например. И этот компонент все сам сделал.
Стоит он 1500 рублей, а шаманства еще на 5000.
ПС.
Отношусь к Вам с большим уважением.
Но как раз вот этот шаманизм в процессе работы с Вашими компонентами, меня реально пугает.
Из-за этого не стал рассматривать Shopmodx.
ППС
Пишу это потому, что была у Вас с Василием один раз перепалка как раз по этому поводу на много постов.
Прошу не обижаться и воспринять эту хотелку Вашего будущего клиента.
Я как быдлокодер испугался. И убежал в слезах. Это какой то лего конструктор получается, собери компонент себе сам))) Видимо я еще не дорос.
Марк, я предполагал такую реакцию. Тем не менее, я постарался максимально подробно все расписать, причем выбрал именно более сложный сценарий. Да, конечно же не всем этот компонент подойдет, тем более, что он устанавливает определенные требования к пониманию MODX на низком уровне. Но я уверен, что со временем выявится группа разработчиков, способных в этом всем разобраться. На самом деле там не все так страшно, как кажется. Кто с ООП знаком, вряд ли сильно испугается. Ну а вопросы по существу всегда можно задавать, я подскажу.
Мысли на счет упрощения есть, и упрощения эти будут. Но именно эта статья будем полезна в дальнейшем для объяснения работы всего механизма. А так, кто-то готов месяц подождать, когда более юзерфрендли версия выйдет, а у кого-то уже проект горит, и знаний хватит под себя допилить, не такой уже и большой объем кода требуется под это.
Мысли на счет упрощения есть, и упрощения эти будут. Но именно эта статья будем полезна в дальнейшем для объяснения работы всего механизма. А так, кто-то готов месяц подождать, когда более юзерфрендли версия выйдет, а у кого-то уже проект горит, и знаний хватит под себя допилить, не такой уже и большой объем кода требуется под это.
Хороший ответ)
А разве ImportX не справляется с задачей импорта, в чём разница? Товары Minishop2, это же обычные ресурсы.
P.S. Еще один момент, это галерея изображений. Как быть с ней?
P.S. Еще один момент, это галерея изображений. Как быть с ней?
ImportX я особо не ковырял, но первое же, что я решил проверить, подтвердилось. Он использует стандартную MODX Console. В своем старом топике я писал
Отмечу только, что это дело очень похоже на стандартный компонент MODx.Console, но это не он. Нативный компонент я попробовал, но отмел из-за того, что он только асинхронные запросы отправляет, не дожидаясь ответа. В общем, морду писал сам.Уточню в чем проблема: тот механизм рассчитан только на один запрос к серверу. Куча остальных запросов идут только на чтение статусов выполнения. И если по таймауту 30 секунд ваш запрос завершится, то импорт встанет колом. В моем решении каждый запрос — это отдельный запрос, так что можно прогружать десятки тысяч товаров, даже если это займет час-два.
P.S. Еще один момент, это галерея изображений. Как быть с ней?Если говорить о минишопе, то можно так же, как и в импортере от Василия.
Купил ваш компонент. Будет подобная инструкция с xml?
Смотрите вот этот топик. Там XLSX, но по сути это тот же самый XML-разбор. Отличие только в том, что перед разбором идет распаковка эксель-файла как архива, а у вас это будет без распаковки сразу чтение из XML.
В любом случае, возникнут вопросы — обращайтесь по существу. Ну и по мере появления у меня очередных кейсов, я так же их буду освещать.
В любом случае, возникнут вопросы — обращайтесь по существу. Ну и по мере появления у меня очередных кейсов, я так же их буду освещать.
Николай, вопрос, что-то понять не могу.
Мне заказчик говорит, пришли мне свое дерево с сайта, я его сопоставлю с деревом в 1с и с вэпэрю итоговый файл.
Зашел msynk — там какая-то околесица, файл дурной вышел.
Проект требует синхронизацию с 1с — и для начала простую выгрузку из minishopa в excel
Вот эту задачу он сможет решить?
Только так, чтобы было видно номер товара в дереве, номер родителя этого товара и поле price
Мне заказчик говорит, пришли мне свое дерево с сайта, я его сопоставлю с деревом в 1с и с вэпэрю итоговый файл.
Зашел msynk — там какая-то околесица, файл дурной вышел.
Проект требует синхронизацию с 1с — и для начала простую выгрузку из minishopa в excel
Вот эту задачу он сможет решить?
Только так, чтобы было видно номер товара в дереве, номер родителя этого товара и поле price
Вот я потому и не стал пока в этот пакет включать выгрузку, потому что пока вообще не понимаю в чем сложности выгрузки. Это же вообще делается довольно просто. Можете для примера посмотреть как сайтмэп формируется в NewsModxBox.
Можете сформировать более четкие требования к вашей выгрузке, написать мне, я вам завтра ее напишу. Сегодня просто занят буду.
Можете сформировать более четкие требования к вашей выгрузке, написать мне, я вам завтра ее напишу. Сегодня просто занят буду.
Продавать компонент, который нужно допиливать как-то некошерно…
Авторизуйтесь или зарегистрируйтесь, чтобы оставлять комментарии.