diff --git a/composer.json b/composer.json index c4f06ff..3b6c4fc 100644 --- a/composer.json +++ b/composer.json @@ -4,23 +4,21 @@ "type": "library", "description": "This extension provides a Multiparser solution for Yii framework 2.0.", "keywords": [ "yii", "parser", "csv", "xml" ], - "homepage": "https://github.com/tsurkanovm/multiparser.git", + "homepage": "https://github.com/tsurkanovm/yii-multiparser.git", "license": "MIT", "authors": [ { "name": "Mihail Tsurkanov", "email": "tsurkanovm@gmail.com", - "homepage": "http://samoinvestor.ru", "role": "Developer" } ], "require": { - "yiisoft/yii2": "*", - "artweb/multiparser": "*" + "yiisoft/yii2": "*" }, "autoload": { "psr-4": { - "yii\\multiparser\\": "" + "artweb\\yii_multiparser\\": "" } } } diff --git a/lib/Converter.php b/lib/Converter.php new file mode 100644 index 0000000..b4e9692 --- /dev/null +++ b/lib/Converter.php @@ -0,0 +1,176 @@ +', '\\'), '', $value_to_convert); + }; + + if( is_string( $value ) ){ + $value = $convert_func( $value ); + } + + if( is_array( $value ) ){ + array_walk( $value, $convert_func ); + } + + return $value; + } + + /** + * @param $name - имя метода конвертации + * @param $value - значение на конвертацию + * @return mixed + */ + public static function __callStatic($name, $value) + { + $method_name = self::METHOD_PREFIX . $name; + + if (method_exists(static::class, $method_name)) { + return static::$method_name($value[0]); + + } else { + // если такого метода конвертации не предусмотрено, то возвращаем не конвертируя + return $value[0]; + + } + } + + public function __call($name, $params) + { + return self::__callStatic($name, $params); + } + + + /** + * @param $arr - массив для конвертирования + * @param $configuration - массив конфигурация конвертирования + * @return mixed + * конвертирует массив по полученным настройкам, вызывая последовательно функции конвертации (указанные в конфигурации) + */ + public static function convertByConfiguration($arr, $configuration) + { + if ($hasKey = isset($configuration['hasKey'])) + unset($configuration['hasKey']); + + if (isset($configuration['configuration'])) { + $arr_config = $configuration['configuration']; + unset($configuration['configuration']); + } else { + throw new \Exception('Не указан обязательный параметр конфигурационного файла - converter_conf[configuration]'); + } + + // проставим аттрибуты из конфига{}{} + self::setAttributes($configuration); + + foreach ($arr_config as $key => $value) { + if ($hasKey) { + // у нас ассоциативный массив, и мы можем конвертировать каждое значение в отдельности + if (is_array($value)) { + //если пустой массив то конвертируем всю строку + if (count($value) === 0) { + + $arr = self::$key($arr); + continue; + } + // иначе конвертируем каждую ячейку в отдельности + foreach ($value as $sub_value) { + if (isset($arr[$sub_value])) { + // конвертируем только те ячейки которые сопоставлены в прочитанном массиве с колонками в конфигурационном файле + $arr[$sub_value] = self::$key($arr[$sub_value]); + } + + } + } else { + + if (isset($arr[$value])) { + // конвертируем только те ячейки которые сопоставлены в прочитанном массиве с колонками в конфигурационном файле + $arr[$value] = self::$key($arr[$value]); + // CustomVarDamp::dump($result); + } + + } + + } else { + // нет заголовка - мы можем конвертировать только строку в целом + $arr = self::$key($arr); + } + + } + + return $arr; + } + + public static function setAttributes($configuration) + { + foreach ($configuration as $key_setting => $setting) { + if (property_exists(static::class, $key_setting)) + static::$$key_setting = $setting; + } + + } + + +} \ No newline at end of file diff --git a/lib/ConverterInterface.php b/lib/ConverterInterface.php new file mode 100644 index 0000000..2d55ab9 --- /dev/null +++ b/lib/ConverterInterface.php @@ -0,0 +1,16 @@ +file->setCsvControl($this->delimiter); + $this->file->setFlags(\SplFileObject::READ_CSV); + $this->file->setFlags(\SplFileObject::SKIP_EMPTY); + + parent::setup(); + + } + + public function read() + { + parent::read(); + + return $this->result; + } + + + protected function readRow( ) + { + $this->row = $this->file->fgetcsv(); + } + + protected function isEmptyRow(){ + + $is_empty = false; + + if ($this->row === false || $this->row === NULL ) { + return true; + } + + $j = 0; + for ($i = 1; $i <= count( $this->row ); $i++) { + + if ( $this->isEmptyColumn( $this->row[$i - 1] ) ) { + $j++; + } + + if ( $j >= $this->min_column_quantity ) { + $is_empty = true; + break; + } + } + + return $is_empty; + } + + protected function isEmptyColumn( $val ){ + return $val == ''; + } +} \ No newline at end of file diff --git a/lib/DynamicFormHelper.php b/lib/DynamicFormHelper.php index 4f337b1..47a0d84 100644 --- a/lib/DynamicFormHelper.php +++ b/lib/DynamicFormHelper.php @@ -6,7 +6,7 @@ * Time: 14:50 */ -namespace yii\multiparser; +namespace artweb\yii_multiparser; use yii\base\DynamicModel; use yii\grid\GridView; diff --git a/lib/Encoder.php b/lib/Encoder.php new file mode 100644 index 0000000..a082765 --- /dev/null +++ b/lib/Encoder.php @@ -0,0 +1,59 @@ + $value) { + $object->$name = $value; + } + + return $object; + } +} \ No newline at end of file diff --git a/lib/Parser.php b/lib/Parser.php new file mode 100644 index 0000000..746e966 --- /dev/null +++ b/lib/Parser.php @@ -0,0 +1,83 @@ +setupConverter(); + } + + protected function setupConverter() + { + if ( $this->has_header_row || $this->keys !== NULL ) { + // если у файла есть заголовок, то в результате имеем ассоциативный массив + $this->converter_conf['hasKey'] = 1; + } + + if ( $this->converter_conf ) { + $converter = ObjectCreator::build( $this->converter_conf ); + if ( $converter instanceof ConverterInterface ) { + + $this->converter = $converter; + + } + } + + + } + + public abstract function read(); + + /** + * @param $arr + * @return mixed + * преобразовует значения прочитанного массива в нужные типы, согласно конфигурации конвертера + */ + protected function convert( $arr ) + { + + if ($this->converter !== NULL) { + + $arr = $this->converter->convertByConfiguration( $arr, $this->converter_conf ); + + } + + + return $arr; + + } +} \ No newline at end of file diff --git a/lib/ParserHandler.php b/lib/ParserHandler.php new file mode 100644 index 0000000..a4908ba --- /dev/null +++ b/lib/ParserHandler.php @@ -0,0 +1,112 @@ +filePath = $filePath; + if (isset($options['mode'])) { + + $this->mode = $options['mode']; + unset($options['mode']); + + } else { + + $this->mode = self::DEFAULT_MODE; + + } + + $this->options = $options; + + $this->fileObject = new \SplFileObject($this->filePath, 'r'); + + $options['file'] = $this->fileObject; + $this->extension = $this->fileObject->getExtension(); + + $this->custom_configuration = $this->getCustomConfiguration($this->extension, $this->mode); + $this->custom_configuration = array_merge_recursive($this->custom_configuration, $options); + + } + + public function run() + { + $result = []; + if (count($this->custom_configuration)) { + + $parser = $this->createObjectByConfiguration($this->custom_configuration); + + try { + + $parser->setup(); + $result = $parser->read(); + + } catch (\ErrorException $e) { + + echo $e->getMessage(); + + } + + } + + return $result; + } + + public function getCustomConfiguration($extension, $parameter) + { + if (!count($this->configuration)) { + $this->setConfiguration(require(__DIR__ . '/config.php')); + } + + if (!isset($this->configuration[$extension])) { + throw new \ErrorException("Parser do not maintain file with extension {$extension}"); + } + if (!isset($this->configuration[$extension][$parameter])) { + throw new \ErrorException("Parser configurator do not have settings for {$parameter} parameter"); + } + + return $this->configuration[$extension][$parameter]; + } + + public function setConfiguration($configuration) + { + $this->configuration = $configuration; + } + + protected function createObjectByConfiguration($configuration) + { + return ObjectCreator::build($configuration); + } +} + + diff --git a/lib/TableParser.php b/lib/TableParser.php new file mode 100644 index 0000000..7a7b7f9 --- /dev/null +++ b/lib/TableParser.php @@ -0,0 +1,173 @@ +auto_detect_first_line) { + $this->shiftToFirstValuableLine(); + } + + // будем считать количество пустых строк подряд - при достижении $empty_lines_quantity - считаем что это конец файла и выходим + $empty_lines = 0; + while ( $empty_lines < $this->empty_lines_quantity ) { + // прочтем строку из файла + $this->readRow(); + + if ( $this->isEmptyRow() ) { + //счетчик пустых строк + $empty_lines++; + continue; + } + + // уберем пустые колонки из ряда + $this->filterRow(); + + + $this->adjustRowToSettings( ); + + // строка не пустая, имеем прочитанный массив значений + $this->current_row_number++; + + // для первой строки утановим ключи из заголовка + $this->setKeysFromHeader(); + + // если у нас установлен лимит, при его достижении прекращаем парсинг + if ( $this->isLastLine() ) + break; + + // обнуляем счетчик, так как считаюся пустые строки ПОДРЯД + $empty_lines = 0; + + $this->result[] = $this->row; + $this->row = []; + + } + + + } + /** + * определяет первую значимую строку, + * считывается файл пока в нем не встретится строка с непустыми колонками + * в количестве указанном в атрибуте min_column_quantity + * в результате выполнения $current_row_number будет находится на последней незначимой строке + */ + protected function shiftToFirstValuableLine() + { + do { + + $this->current_row_number ++; + $this->readRow(); + + } while( $this->isEmptyRow() ); + + // @todo - сделать опционально + // код для того что бы парсить первую строку, закомментировано как предполагается что первая значимая строка это заголовок + // $this->current_row_number --; +// $this->file->seek( $this->current_row_number ); + } + + /** + * @return array - одномерный массив результата парсинга строки + */ + protected function adjustRowToSettings( ) + { + + // если есть заголовок, то перед конвертацией его нужно назначить + if ( $this->keys !== NULL ) { + + if (count($this->keys) !== count($this->row)) { + throw new \Exception("Ошибка парсинга файла в строке # {$this->current_row_number}. Не соответсвие числа ключевых колонок (заголовка) - числу колонок с данными", 0, 1, $this->file->getBasename(), $this->current_row_number); + } + + $this->row = array_combine($this->keys, $this->row); + } + + // попытаемся конвертировать прочитанные значения согласно конфигурации котнвертера значений + $this->row = $this->convert($this->row); + + // обрежем массив к первой значимой колонке + if ( $this->first_column ) { + + $this->row = array_slice($this->row, $this->first_column); + + } + + } + + protected function setKeysFromHeader(){ + if ( $this->has_header_row ) { + // в файле есть заголовок, но он еще не назначен - назначим + if ($this->keys === NULL) { + $this->keys = array_values( $this->row ); + } + } + } + + protected function filterRow(){ + $this->row = array_filter( $this->row, function($val){ + return !$this->isEmptyColumn($val); + }); + } + + protected function isLastLine(){ + + if ( ( $this->last_line ) && ( $this->current_row_number > $this->last_line ) ) { + return true; + } + return false; + } + +} \ No newline at end of file diff --git a/lib/XlsxParser.php b/lib/XlsxParser.php new file mode 100644 index 0000000..0c48f00 --- /dev/null +++ b/lib/XlsxParser.php @@ -0,0 +1,178 @@ +path_for_extract_files == '' ) { + $this->path_for_extract_files = sys_get_temp_dir(); + } + } + + + public function read() + { + + // $this->extractFiles(); + + $this->readSheets(); + $this->readStrings(); + + foreach ( $this->sheets_arr as $sheet ) { + //проходим по всем файлам из директории /xl/worksheets/ + + $sheet_path = $this->path_for_extract_files . '/xl/worksheets/' . $sheet . '.xml'; + if ( file_exists( $sheet_path ) && is_readable( $sheet_path ) ) { + + $xml = simplexml_load_file( $sheet_path, "SimpleXMLIterator" ); + $this->current_node = $xml->sheetData->row; + $this->current_node->rewind(); + + parent::read(); + + } + + } +CustomVarDamp::dumpAndDie($this->$result); + // return $this->$result_arr; + } + + protected function extractFiles () + { + $zip = new \ZipArchive; + if ( $zip->open( $this->file->getPathname() ) === TRUE ) { + $zip->extractTo( $this->path_for_extract_files ); + $zip->close(); + } else { + throw new \Exception( 'Ошибка чтения xlsx файла' ); + } + } + + protected function readSheets () + { + if ( $this->active_sheet ) { + $this->sheets_arr[ $this->active_sheet ] = 'Sheet' . $this->active_sheet; + return; + } + + $xml = simplexml_load_file( $this->path_for_extract_files . '/xl/workbook.xml' ); + foreach ( $xml->sheets->children() as $sheet ) { + $sheet_name = ''; + $sheet_id = 0; + $attr = $sheet->attributes(); + foreach ( $attr as $name => $value ) { + if ($name == 'name') + $sheet_name = (string)$value; + + if ($name == 'sheetId') + $sheet_id = $value; + + } + if ( $sheet_name && $sheet_id ) { + $this->sheets_arr[$sheet_name] = 'Sheet' . $sheet_id; + } +// + } + } + + protected function readStrings () + { + $xml = simplexml_load_file( $this->path_for_extract_files . '/xl/sharedStrings.xml' ); + foreach ( $xml->children() as $item ) { + $this->strings_arr[] = (string)$item->t; + } + } + + + + // protected function readRow ( $item, $sheet , $current_row ) + protected function readRow ( ) + { + $node = $this->current_node->getChildren(); + + foreach ( $node as $child ) { + $attr = $child->attributes(); + + if( isset($child->v) ) { + $value = (string)$child->v; + }else{ + $value = ''; + } + if ( isset( $attr['t'] ) ) { + // $this->result_arr[$sheet][$current_row][$cell] = $this->strings_arr[ $value ]; + $this->row[] = $this->strings_arr[ $value ]; + }else{ + // $this->result_arr[$sheet][$current_row][$cell] = $value; + $this->row[] = $value; + } + + } + $this->current_node->next(); + CustomVarDamp::dump($this->row); + } + + protected function isEmptyRow(){ + + $is_empty = false; + + if ( !count( $this->row ) || !$this->current_node->valid() ) { + return true; + } + + $j = 0; + for ($i = 1; $i <= count( $this->row ); $i++) { + + if ( $this->isEmptyColumn( $this->row[$i - 1] ) ) { + $j++; + } + + if ( $j >= $this->min_column_quantity ) { + $is_empty = true; + break; + } + } + + return $is_empty; + } + + protected function isEmptyColumn( $val ){ + return $val == ''; + } +} \ No newline at end of file diff --git a/lib/XmlParser.php b/lib/XmlParser.php new file mode 100644 index 0000000..7c0cfac --- /dev/null +++ b/lib/XmlParser.php @@ -0,0 +1,118 @@ +file; + $result = $this->xmlToArray( $file->getPathname() ); + + if ( isset($this->node) ) { + + $result = $result[ $this->node ]; + + } + + return $result; + } + + + /** + * Converts an XML string to a PHP array + * + * @uses recursiveXMLToArray() + * @param string $file_path + * @return array + */ + protected function xmlToArray( $file_path ) { + + try { + $xml = new \SimpleXMLElement( $file_path, 0, true ); + //\common\components\CustomVarDamp::dumpAndDie($xml->children()->children()); + $result = $this->recursiveXMLToArray( $xml ); + } catch(Exception $ex) { + + throw $ex; + } + + return $result; + } + + /** + * Convert a XML string to a PHP array recursively. Do not + * call this function directly + * + * @param SimpleXMLElement + * + * @return mixed + */ + protected function recursiveXMLToArray($xml) { + if( $xml instanceof \SimpleXMLElement ) { + $attributes = $xml->attributes(); + + foreach( $attributes as $key => $value ) { + if( $value ) { + $attribute_array[$key] = (string) $value; + } + } + $previous_xml = $xml; + $xml = get_object_vars($xml); + } + + if(is_array($xml)) { + + if( count($xml) == 0 ) + return (string) $previous_xml; // for CDATA + + foreach($xml as $key => $value) { + $row[$key] = $this->recursiveXMLToArray($value); + } + if ( is_string($value) ) { + // дошли до конца рекурсии + // преобразуем ряд согласно конфигурации + if ( $this->keys !== NULL ) { + // назначим ключи из конфигурации, согласно массиву $keys + $row = $this->compareArrayWithKeys( $row ); + } + $row = $this->convert( $row ); + + } + + + if( isset( $attribute_array ) ) + $row['@'] = $attribute_array; // Attributes + + return $row; + } + return (string) $xml; + } + + /** + * @param array $value_arr - текущий ряд, массив, которому нужно назначить конфигурационные ключи ($keys) + * @return array + */ + protected function compareArrayWithKeys( array $value_arr ){ + $res = $this->keys; + foreach ( $this->keys as $key => $value ) { + if ( array_key_exists( $value, $value_arr ) ) { + $res[$key] = $value_arr[$value]; + } + } + return $res; + } + +} \ No newline at end of file diff --git a/lib/YiiConverter.php b/lib/YiiConverter.php new file mode 100644 index 0000000..b017e8b --- /dev/null +++ b/lib/YiiConverter.php @@ -0,0 +1,51 @@ +configuration ); + if ( $converter instanceof ConverterInterface ) { + + $this->converter = $converter; + }else{ + throw new ErrorException('Wrong type of converter'); + } + + + } + + public function convertTo( $method, $value, $attributes = [] ){ + + if ( $attributes ) { + $this->converter->setAttributes($attributes); + } + return $this->converter->$method( $value ); + + } + + public function convertByConfiguration( $value, $configuration ){ + + return $this->converter->convertByConfiguration( $value, $configuration ); + + } + + +} \ No newline at end of file diff --git a/lib/YiiMultiparser.php b/lib/YiiMultiparser.php index 58ff2d7..7f93ec5 100644 --- a/lib/YiiMultiparser.php +++ b/lib/YiiMultiparser.php @@ -6,7 +6,7 @@ * Time: 15:56 */ -namespace yii\multiparser; +namespace artweb\yii_multiparser; use common\components\CustomVarDamp; use yii\base\Component; @@ -17,27 +17,29 @@ use yii\base\Component; class YiiMultiparser extends Component{ public $configuration; +public $parserHandler; - public function getConfiguration($extension, $conf_parameter){ - - if (!isset( $this->configuration[$extension] )){ - throw new \ErrorException( "Parser do not maintain file with extension {$extension}"); - } - if (!isset( $this->configuration[$extension][$conf_parameter] )){ - throw new \ErrorException( "Parser configurator do not have settings for {$conf_parameter} parameter"); - } - - return $this->configuration[$extension][$conf_parameter]; + public function init() + { + parent::init(); + $this->parserHandler = new YiiParserHandler( ); + $this->parserHandler->setConfiguration( $this->configuration ); } public function parse( $filePath, $options = [] ){ - $parser = new YiiParserHandler( $filePath, $options ); - return $parser->run(); + $this->parserHandler->setup( $filePath, $options ); + + return $this->parserHandler->run(); } + public function getConfiguration( $extension, $parameter ){ + + return $this->parserHandler->getCustomConfiguration( $extension, $parameter ); + + } } \ No newline at end of file diff --git a/lib/YiiParserHandler.php b/lib/YiiParserHandler.php index 5bf86ad..9baedd4 100644 --- a/lib/YiiParserHandler.php +++ b/lib/YiiParserHandler.php @@ -6,7 +6,7 @@ * Time: 15:53 */ -namespace yii\multiparser; +namespace artweb\yii_multiparser; use common\components\CustomVarDamp; @@ -17,68 +17,74 @@ class YiiParserHandler extends ParserHandler{ /** * @param $filePath * @param array $options - * проверяет читабенльность переданного файла, а также наличие настроек парсера в конфигурационном файле для данного типа файла + * проверяет читабельность переданного файла, а также наличие настроек парсера в конфигурационном файле для данного типа файла */ - public function __construct($filePath, $options = []) +// public function setup($filePath, $options = []) +// { +// $this->filePath = $filePath; +// if (isset($options['mode'])) { +// +// $this->mode = $options['mode']; +// unset($options['mode']); +// +// } else { +// +// $this->mode = self::DEFAULT_MODE; +// +// } +// +// $this->options = $options; +// +// try { +// $this->fileObject = new \SplFileObject($this->filePath, 'r'); +// } catch (\ErrorException $e) { +// // Yii::warning("Ошибка открытия файла {$this->filePath}"); +// echo "Ошибка открытия файла {$this->filePath}"; +// return []; +// } +// +// $options['file'] = $this->fileObject; +// $this->extension = $this->fileObject->getExtension(); +// +// try { +// +// $this->configuration = array_merge_recursive ($this->configuration, $options); +// +// } catch (\ErrorException $e) { +// echo $e->getMessage(); +// return []; +// } +// +// } +// +// public function run() +// { +// +// $result = []; +// +// // \common\components\CustomVarDamp::dumpAndDie($this); +// if (count($this->configuration)) { +// $parser = \Yii::createObject($this->configuration); +// +// try { +// +// $parser->setup(); +// $result = $parser->read(); +// +// } catch (\ErrorException $e) { +// +// echo $e->getMessage(); +// +// } +// +// } +// +// return $result; +// } + protected function createObjectByConfiguration($configuration) { - $this->filePath = $filePath; - if (isset($options['mode'])) { - - $this->mode = $options['mode']; - unset($options['mode']); - - } else { - - $this->mode = self::DEFAULT_MODE; - - } - - $this->options = $options; - - try { - $this->fileObject = new \SplFileObject($this->filePath, 'r'); - } catch (\ErrorException $e) { - // Yii::warning("Ошибка открытия файла {$this->filePath}"); - echo "Ошибка открытия файла {$this->filePath}"; - return []; - } - - $options['file'] = $this->fileObject; - $this->extension = $this->fileObject->getExtension(); - - try { - $this->configuration = \Yii::$app->multiparser->getConfiguration($this->extension, $this->mode); - $this->configuration = array_merge_recursive ($this->configuration, $options); - } catch (\ErrorException $e) { - echo $e->getMessage(); - return []; - } - + return \Yii::createObject($configuration); } - public function run() - { - - $result = []; - - // \common\components\CustomVarDamp::dumpAndDie($this); - if (count($this->configuration)) { - $parser = \Yii::createObject($this->configuration); - - try { - - $parser->setup(); - $result = $parser->read(); - - } catch (\ErrorException $e) { - - echo $e->getMessage(); - - } - - } - - return $result; - } } \ No newline at end of file diff --git a/lib/config.php b/lib/config.php new file mode 100644 index 0000000..7dbd31e --- /dev/null +++ b/lib/config.php @@ -0,0 +1,20 @@ + + ['web' => + ['class' => 'artweb\yii_multiparser\CsvParser', + 'auto_detect_first_line' => true, + 'converter_conf' => [ + "float" => 'PRICE', + "integer" => 'QUANTITY', + "string" => 'DESCR' + ]], + 'basic_column' => [ + "ARTICLE" => 'Артикул', + "PRICE" => 'Цена', + "DESCR" => 'Наименование', + "QUANTITY" => 'Колво' + + ], + ]]; -- libgit2 0.21.4