Версия: 6.x
burger close
Разработка схем импорта/экспорта в CSV/XLS/XLSX/ODS

ReadyScript предоставляет готовую подсистему для быстрой разработки схем импорта/экспорта данных в форматах CSV, XLS, XLSX, ODS.

  • Схема импорта/экспорта - описывает общие параметры импорта/экспорта и набор пресетов.
  • Пресет - отвечает за экспорт и импорт группы колонок.

Посмотреть, как работает импорт/экспорт можно практически в любом разделе административной панели. Соответствующая кнопка есть в правой верхней части различных страниц, например в разделе Товары -> Каталог товаров.

import-export.png
Кнопка Импорт/Экспорт

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

export-dialog.png
Диалог выбора колонок для экспорта

Любой файл, сформированный через инструмент экспорта, можно импортировать обратно в систему и тем самым либо создать, либо обновить объекты в системе. Для импорта файла, следует выбрать пункт, например, Импорт товаров и затем выбрать файл. После анализа данных первой строки загружаемого файла, система автоматически предложит сопоставление имеющихся в файле колонок с колонками, которые известны системе. Сопоставив колонки, нужно нажать на кнопку "Начать импорт", после чего начнется загрузка данных из файла.

import-dialog.png
Диалог выбора колонок для импорта

Схема экспорта

Схема экспорта - это потомок класса RS::Csv::AbstractSchema. Схема экспорта обеспечивает выполнение экспорта, запрашивая поочередно у пресетов данные слева-направо (если смотреть на итоговую таблицу файла). Каждый пресет подготавливает данные для одной или нескольких колонок. Первым всегда отрабатывает базовый пресет, затем все дополнительные.

В случае выполнения импорта данных, запуск функции импорта данных происходит у пресетов справа-налево (если смотреть на таблицу файла), базовый пресет в данном случае получает управление самым последним.

import-export-directions.jpg
Направление экспорта/импорта

Схемы импорта/экспорта должны располагаться в следующих папках: /modules/ИМЯ-МОДУЛЯ/model/csvschema/

Рассмотрим аргументы конструктора схемы импорта/экспорта.

<?php
namespace Catalog\Model\CsvSchema;
use RS\Csv\Preset;
/**
* Пример схемы экспорта/импорта категорий товаров
*/
class Dir extends AbstractSchema
{
function __construct()
{
//Базовый пресет - описывает базовый выгружаемый/загружаемый ORM-объект
$base_preset = new Preset\Base([
'ormObject' => new \Catalog\Model\Orm\Dir(), //Экспортируемый ORM-объект
'multisite' => true, //Объект поддерживает мультисайтовость
'searchFields' => ['name', 'parent'], //Колонки, по которым будет поиск объекта и принятие решения о его создании или обновления
'nullFields' => ['sortn'], //Устанавливает колонки, которые в случае пустоты будут записаны в базу как NULL
]);
//Описывает дополнительные пресеты, которые привносят в CSV группы колонок
$additional_presets = [
//Пресет, обрабатывающий выгрузку/загрузку изображения из поля с типом RS\Orm\Type\Image
new Preset\SinglePhoto([
'linkPresetId' => 0, //Указывает, в какой пресет (0 - базовый пресет) записать значение, после загрузки изображения
'linkForeignField' => 'image', //Указывает, в какое поле записать значение, после загрузки изображения
'title' => t('Изображение'), //Название колонки в итоговом файле
]),
//Пресет, обрабатывающий выгрузку/загрузку сложного поля "Родитель" с цепочкой родительских категорией, разделенных символом / (слэш)
new Preset\TreeParent([
'ormObject' => new \Catalog\Model\Orm\Dir(), //ORM-объект, описывающий один элемент иерархии
'titles' => [
'name' => t('Родитель'), //Устанавливает название для колонки в итоговом файле
],
'idField' => 'id', //Поле, описывающее ID категории
'parentField' => 'parent', //Поле, содержащее указатель на родительскую категорию
'treeField' => 'name', //Поле, содержащие название категории
'rootValue' => 0, //ID корневой категории
'multisite' => true, //Объект - поддерживает мультисайтовость
'linkForeignField' => 'parent', //Поле, в которое нужно записывать итоговый ID родительской категории
'linkPresetId' => 0, //Пресет, в который записать значение (0 - базовый)
]),
];
//Необязательные параметры
$options = [
'baseIdField' => 'id', //Поле, содержащее ID базового пресета
'baseQuery' => null, //Объект RS\Orm\Request с выборкой базовых объектов ORM или null
'importSkipFirst' => true, //True - пропускать первую строку при импорте
'beforeLineImport' => $callable, //callback(AbstractSchema $this), вызывается после чтения строки данных и непосредственно перед импортом
'afterLineImport' => $callable, //callback(AbstractSchema $this), вызывается после импорта данных
'afterImport' => $callable, //callback(AbstractSchema $this), вызывается после импорта порции строк, равных одному шагу
];
parent::__construct($base_preset, $additional_presets, $options);
}
}

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

Пресет

Пресет - это потомок класса RS::Csv::Preset::AbstractPreset, привносящий в схему экспорта одну или несколько колонок для импорта/экспорта. Рекомендуемый путь для размещения файла с классом: /modules/ИМЯ-МОДУЛЯ/model/csvpreset/

Рассмотрим абстрактный пример реализации пресета.

Каждый пресет содержит 3 обязательных метода для реализации:

  • getColumns - должен возвращать массив привносимых в схему колонок
  • getColumnsData - должен возвращать экспортируемые данные для этих колонок
  • importColumnsData - должен производить импорт данных по своим колонкам
<?php
namespace Modulename\Model\CsvPreset;
/**
* Пресет для экспорта данных (полный url товара)
*/
class ExamplePreset extends AbstractPreset
{
/**
* Справочник цветов
*/
protected array $colors;
/*
* Вызывается непосредственно перед импортом одной порции данных.
* Можно использовать для предварительной загрузки справочников или иных данных.
* Необязательный метод
*
* @return void
*/
public function loadData()
{
$this->colors = [
1 => t('Зеленый'),
2 => t('Синий'),
3 => t('Красный'),
]
}
/**
* Возвращает список колонок, которые пресет привносит в схему импорта/экспорта
*
* @return array
*/
public function getColumns()
{
return [
//Обязательно использовать префикс $this->id в ключе поля для уникализации ключей колонок именно этого пресета
//somefield - произвольный идентификатор, характеризующий колонку
$this->id . '-color-id' => [ //Общий ключ колонки в рамках всей схемы
'key' => 'color-id', //Ключ колонки именно этого пресета
'title' => t('ID цвета') //Название колонки
],
$this->id . '-color-title' => [ //Общий ключ колонки в рамках всей схемы
'key' => 'color-title', //Ключ колонки именно этого пресета
'title' => t('Цвет') //Название колонки
]
];
}
/**
* Возвращает данные для экспорта колонок, за которые отвечает пресет
*
* @param integer $n - номер строки в импортируемой порции данных
* @return array
*/
public function getColumnsData($n)
{
//Получим данные ORM-объекта из базового пресета схемы для строки $n
//Можем его как-то использовать для формирования данных
$base_orm_object = $this->schema->rows[$n];
return [
$this->id . '-color-id' => $base_orm_object['color_id'],
$this->id . '-color-title' => $this->colors[$base_orm_object['color_id']] ?? t('Неизвестный цвет'),
];
}
/**
* Метод обеспечивает импорт данных своих колонок
*
* @return void
*/
public function importColumnsData()
{
// В переменной $this->row, ReadyScript сразу подготовит данные, загруженные из импортируемого файла именно для
// этого пресета. Но также всегда можно обращаться и к значениям колонок других пресетов через $this->schema->getPreset($index)->row,
// где $index - это порядковый номер пресета, 0 - базовый.
$color_id = $this->row['color-id'] ?? null; //здесь будет значение из колонки "ID цвета"
$color_title = $this->row['color-title'] ?? null; //здесь будет значение из колонки "Цвет"
if ($color_title !== null) {
$flipped_colors = array_flip($this->colors); //Перевернем наз справочник, чтобы он получил вид ['Зеленый' => 1, 'Синий' => 2, ...]
if (isset($flipped_colors[ $color_title ]) { //Если $color_title есть в нашем справочнике цветов
//То установим в базовый пресет (0), в колонку color_id, ID цвета из нашего справочника.
$this->schema->getPreset(0)->row['color_id'] = $flipped_colors[ $color_title ];
}
}
}
}

В ReadyScript есть набор базовых классов-пресетов, которые покрывают основные сценарии организации экспорта/импорта:

  • RS::Csv::Preset::LinkedTable - Позволяет организовать импорт/экспорт колонок из связанной через один ко многим таблицы
  • RS::Csv::Preset::ManyToMany - Позволяет организовать импорт/экспорт колонок из другой таблицы, связанной с основным объектом через связь многие ко многим
  • RS::Csv::Preset::ManyTreeParent - Позволяет организовать импорт/экспорт колонки, описывающей связь многие ко многим с древовидным списком
  • RS::Csv::Preset::PhotoBlock - Позволяет организовать импорт/экспорт колонки с фотографиями из модуля Блок фотографий
  • RS::Csv::Preset::ProductsSerialized - Позволяет организовать импорт/экспорт колонки с группами и товарами из диалога выбора групп и товаров
  • RS::Csv::Preset::SerializedArray - Позволяет организовать импорт/экспорт колонки, сериализованного плоского массива ключ => значения
  • RS::Csv::Preset::SinglePhoto - Позволяет организовать импорт/экспорт колонки с фотографией из поля RS
  • RS::Csv::Preset::TreeParent - Позволяет организовать импорт/экспорт колонки, содержащей путь к родительской древовидной категории

Примеры использования данных пресетов можно найти в многочисленных схемах в системных модулях.

Пример вызова экспорта и импорта данных из PHP

В некоторых случаях может понадобиться формировать CSV-экспортный файл не из административной панели, а через PHP-скрипт.

Рассмотрим пример вызова механизма экспорта данных о товаре в CSV из PHP-скрипта "export.php", созданного в корне сайта.

<?php
require('setup.inc.php');
$destination_file = __DIR__.'/export.csv';
$schema = new \Catalog\Model\CsvSchema\Product(); //Создаем объект схемы экспорта
$schema->setFormat( \RS\Csv\Format\Type\Csv::getId() ); //Устанавливаем формат экспорта данных CSV
$schema->setCharset( 'UTF-8' ); //Устанавливаем кодировку
//Опциональный метод, позволяющий указать поля, которые будут экспортированы в файл
$schema->setWorkFields([
'0-title',
'0-alias',
'0-description'
//Тут можно перечислить ключи колонок из результата вызова $schema->getColumns()
]);
//Запускаем экспорт данных в CSV
$schema->exportToFile($destination_file);

Также рассмотрим пример импорта CSV файла из PHP-скрипта "import.php", созданного в корне сайта.

<?php
require('setup.inc.php');
$source_file = __DIR__.'/export.csv';
$schema = new \Catalog\Model\CsvSchema\Product();
$schema->setCharset('UTF-8');
//Опциональный метод, позволяющий указать поля, которые будут импортированы в файл
$schema->setWorkFields([
'0-title', //первая колонка в CSV файле будет соответствовать колонке "Короткое название"
'0-alias', //вторая колонка в CSV файле будет соответствовать колонке "URL-имя"
'0-description' //третья колонка в CSV файле будет соответствовать колонке "Описание"
//Тут можно перечислить колонки из ключей $schema->getColumns()
]);
//Формат файла будет определен автоматически по расширению файла
$schema->import($source_file);

События

csv.scheme.afterconstruct.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается после выполнения стандартного конструктора схемы

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • scheme - потомок класса , объект схемы, которая вызвала данное событие

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

Для этого следует создать обработчик события csv.scheme.afterconstruct.КОРОТКОЕ-ИМЯ-СХЕМЫ, где КОРОТКОЕ-ИМЯ-СХЕМЫ формируется путем исключения из полного наименования класса в нижнем регистре секции и замены обратных слешей на знак "-", с обрезкой минусов по краям.

Например, короткое имя схемы для класса будет выглядеть так: shop-orderitems. А полный идентификатор события будет соответственно выглядеть так: csv.scheme.afterconstruct.shop-orderitems

Рассмотрим пример добавления собственного пресета (набора колонок) из условного модуля Custom в системную схему экспорта заказанных товаров.

Файл /modules/custom/config/handlers.inc.php

<?php
namespace Custom\Config;
/**
* Класс обеспечивает обработку события
*/
class Handlers extends \RS\Event\HandlerAbstract
{
/**
* Метод отвечает за установку подпискиков на события
*/
function init()
{
//Будем расширять схему экспорта \Shop\Model\CsvSchema\OrderItems
$this->bind('csv.scheme.afterconstruct.shop-orderitems');
}
/**
* Обработчик события csv.scheme.afterconstruct.shop-orderitems
*/
public static function csvSchemeAfterConstructShopOrderItems($params)
{
/**
* @var $scheme \Shop\Model\CsvSchema\OrderItems
*/
$scheme = $params['scheme'];
//Добавляем кастомный пресет с набором колонок
$scheme->addPreset( new Custom\Model\CsvPreset\OrderWarehouse([]) );
}
}

Файл /modules/custom/model/csvpreset/orderwarehouse.inc.php

<?php
namespace Custom\Model\CsvPreset;
/**
* CSV пресет, привносящий одну колонку - склад заказа в схему экспорта
*/
class OrderWarehouse extends AbstractPreset
{
/**
* Возвращает ассоциативный массив с одной строкой данных, где ключ - это id колонки, а значение - это содержимое ячейки
*
* @param integer $n - индекс в наборе строк $this->rows
* @return array
*/
public function getColumnsData($n)
{
$order_id = $this->schema->rows[$n]['order_id'];
$order = new Order($order_id, false);
$warehouse = $order->getWarehouse();
return [
$this->id.'-order_warehouse' => $warehouse['title']
];
}
/**
* Должен импортировать одну строку данных, но необязательно
*
* @return void
*/
public function importColumnsData()
{
//Ничего не делаем, так как данный пресет предназначен только для экспорта данных
}
/**
* Возвращает колонки, которые добавляются текущим пресетом
*
* @return array
*/
public function getColumns()
{
return [
$this->id.'-order_warehouse' => [
'key' => 'order_warehouse',
'title' => t('Заказ со склада')
]
];
}
}

csv.beforelineexport.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается перед экспортом одной строки данных

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • schema - потомок класса , объект схемы, которая вызвала данное событие
  • row_index - integer, номер экспортируемой строки

csv.beforeimport.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается перед началом импорта одного файла

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • schema - потомок класса , объект схемы, которая вызвала данное событие

csv.beforelineimport.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается перед началом импорта одной строки данных. Если событие будет остановлено, то импорт строки будет пропущен

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • schema - потомок класса , объект схемы, которая вызвала данное событие

csv.afterlineimport.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается после импорта одной строки данных

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • schema - потомок класса , объект схемы, которая вызвала данное событие

csv.afterimport.КОРОТКОЕ-ИМЯ-СХЕМЫ

Вызывается после импорта одной порции данных. Одна порция - это объем импортированных строк данных, который успел загрузиться за время до истечения timeout (обычно 20 секунд).

Тип входящего параметра: array
Описание входящего параметра: Элементы массива:
  • schema - потомок класса , объект схемы, которая вызвала данное событие

Связь экспорта c отбором данных в административной панели

Иногда очень удобно связывать экспорт данных с фильтрами, установленными в административной панели в момент запуска экспорта. Например, вы отфильтровали заказы по статусу "Завершено", нажимаете экспорт заказов в CSV и получаете в CSV файле только те заказы, которые видели в административной панели после применения фильтров.

Для того, чтобы установить такую связь, достаточно в классе схемы экспорта установить у базового пресета через параметр savedRequest передать уже инициализированный фильтрами объект RS::Orm::Request.

Рассмотрим на примере класса схемы экспорта валют:

<?php
namespace Catalog\Model\CsvSchema;
use \RS\Csv\Preset;
/**
* Схема экспорта/импорта справочника цен в CSV
*/
class Currency extends \RS\Csv\AbstractSchema
{
function __construct()
{
parent::__construct(new Preset\Base([
'ormObject' => new \Catalog\Model\Orm\Currency(),
'excludeFields' => [
'id', 'site_id'
],
//Устанавливаем объект выборки валют прямо из сессии.
'savedRequest' => \Catalog\Model\CurrencyApi::getSavedRequest('Catalog\Controller\Admin\CurrencyCtrl_list'),
'multisite' => true,
'searchFields' => ['title']
]));
}
}

Если для вывода списка объектов используются стандартные для ReadyScript подходы, т.е. список объектов отображается с помощью контроллера-потомка класса RS::Controller::Admin::Crud, то получить объект последней выборки RS::Orm::Request со всеми условиями этой выборки можно из DAO-объекта, связанного с контроллером. В нашем примере - это Catalog::Model::CurrencyApi ,потомок RS::Module::AbstractModel::EntityList.

Метод getSavedRequest($key, $default = null), есть у всех потомков RS::Module::AbstractModel::EntityList, он принимает два аргумента на вход:

  • string $key - идентификатор сохраненной выборки. Состоит из Полного имени класса контроллера и постфикса "_list"
  • mixed $default - значение по умолчанию, будет возвращено, если в сессии ничего для такого ключа $key не будет найдено

Добавляем кнопку импорт/экспорт в административной панели

Если вы создаете стандартный контроллер административной панели, построенный на базе класса RS::Controller::Admin::Crud, то для добавления кнопки Импорт/Экспорт достаточно воспользоваться методом addCsvButton у класса RS::Controller::Admin::Helper::CrudCollection, с помощью которого вы настраиваете состав элементов страницы.

Пример:

<?php
namespace Catalog\Controller\Admin;
/**
* Контроллер раздела Товары -> Справочник цен
*/
class CostCtrl extends \RS\Controller\Admin\Crud
{
function __construct()
{
parent::__construct(\Catalog\Model\Costapi::getInstance());
}
function helperIndex()
{
$helper = parent::helperIndex();
$helper->setTopTitle(t('Справочник типов цен'));
$helper->addCsvButton('catalog-typecost'); //Добавляем кнопку импорт/экспорт для данной схемы
//...
return $helper;
}
}

Примечание

Заметки
Это важно: CSV формат не требователен к объему памяти (опции PHP memory_limit) и отличается высокой скоростью работы, в то время как остальные форматы для обработки больших данных требуют на порядки большего объема памяти. Для выгрузкии и загрузки больших объемов данных рекомендуем использовать CSV формат.