Add CSV export
This commit is contained in:
parent
347de8eeaf
commit
f9829d4aea
2 changed files with 338 additions and 0 deletions
240
src/Export/CSV.php
Normal file
240
src/Export/CSV.php
Normal file
|
@ -0,0 +1,240 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace EesyPHP\Export;
|
||||||
|
|
||||||
|
use EesyPHP\Date;
|
||||||
|
use EesyPHP\Log;
|
||||||
|
|
||||||
|
use function EesyPHP\implode_with_keys;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CSV export
|
||||||
|
* @property-read string $separator
|
||||||
|
* @property-read string $enclosure
|
||||||
|
* @property-read string $escape
|
||||||
|
* @property-read string $eol
|
||||||
|
* @property-read int|null $max_length
|
||||||
|
* @property-read string $date_format
|
||||||
|
* @property-read bool|string $trim
|
||||||
|
*/
|
||||||
|
class CSV extends Generic {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of fields in export. Could be an associative array to specify custom exporting
|
||||||
|
* parameters:
|
||||||
|
* [
|
||||||
|
* 'name',
|
||||||
|
* 'name' => 'label',
|
||||||
|
* 'name' => [
|
||||||
|
* 'label' => 'Name',
|
||||||
|
* 'to_string' => [callable],
|
||||||
|
* 'from_string' => [callable],
|
||||||
|
* ],
|
||||||
|
* ].
|
||||||
|
* @var array<string,string>|array<string>
|
||||||
|
*/
|
||||||
|
protected $fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of export parameters default value
|
||||||
|
* @var array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected static $default_parameters = [
|
||||||
|
"separator" => ",", // The fieds separator character
|
||||||
|
"enclosure" => "\"", // The enclosure character
|
||||||
|
"escape" => "\\", // The enclosure character
|
||||||
|
"eol" => "\n", // The end-of-line character. Note: only used in PHP >= 8.1.0.
|
||||||
|
// Line max length, see fgetcsv()
|
||||||
|
"max_length" => PHP_VERSION_ID >= 80000?null:99999,
|
||||||
|
// DateTime object exporting format
|
||||||
|
"date_format" => 'Y/m/d H:i:s',
|
||||||
|
// Specify if values loaded from CSV file have to be trim. Could be a boolean or a string of the
|
||||||
|
// stripped characters.
|
||||||
|
"trim" => true,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute fields mapping info
|
||||||
|
* @return array<string,mixed> Fields mapping info
|
||||||
|
*/
|
||||||
|
protected function _fields_mapping() {
|
||||||
|
$map = [];
|
||||||
|
foreach(parent :: _fields_mapping() as $key => $info) {
|
||||||
|
$map[$key] = [
|
||||||
|
'label' => $info['label'],
|
||||||
|
'to_string' => (
|
||||||
|
array_key_exists('to_string', $info)?
|
||||||
|
$info['to_string']:[$this, 'to_string']
|
||||||
|
),
|
||||||
|
'from_string' => (
|
||||||
|
array_key_exists('from_string', $info)?
|
||||||
|
$info['from_string']:[$this, 'from_string']
|
||||||
|
),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert value to string as expected in the export
|
||||||
|
* @param mixed $value The value to export
|
||||||
|
* @return string The value as string
|
||||||
|
*/
|
||||||
|
protected function to_string($value) {
|
||||||
|
if (is_null($value))
|
||||||
|
return '';
|
||||||
|
if (is_a($value, "\DateTime"))
|
||||||
|
return Date :: format($value, $this -> date_format);
|
||||||
|
return strval($value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert value from string
|
||||||
|
* @param string $value The value to convert
|
||||||
|
* @return mixed The converted value
|
||||||
|
*/
|
||||||
|
protected function from_string($value) {
|
||||||
|
if ($this -> trim) {
|
||||||
|
if (is_string($this -> trim))
|
||||||
|
$value = trim($value, $this -> trim);
|
||||||
|
else
|
||||||
|
$value = trim($value);
|
||||||
|
}
|
||||||
|
return empty($value)?null:$value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fputcsv wrapper in context of this export
|
||||||
|
* @param resource $fd The file pointer of the export
|
||||||
|
* @param array<string> $fields The field of the row to export
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function fputcsv($fd, $fields) {
|
||||||
|
$args = [$fd, $fields, $this -> separator, $this -> enclosure, $this -> escape];
|
||||||
|
if (PHP_VERSION_ID >= 80100)
|
||||||
|
$args[] = $this -> eol;
|
||||||
|
return call_user_func_array('fputcsv', $args) !== false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* fgetcsv wrapper in context of this export
|
||||||
|
* @param resource $fd The file pointer of the import
|
||||||
|
* @return array<string>|false The retrieved row or false in case of error or at the end of the
|
||||||
|
* file
|
||||||
|
*/
|
||||||
|
public function fgetcsv($fd) {
|
||||||
|
return fgetcsv(
|
||||||
|
$fd,
|
||||||
|
$this -> max_length,
|
||||||
|
$this -> separator,
|
||||||
|
$this -> enclosure,
|
||||||
|
$this -> escape
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export items
|
||||||
|
* @param array<array<string,mixed>> $items The items to export
|
||||||
|
* @param resource|null $fd The file pointer where to export (optional, default: php://output)
|
||||||
|
* @return boolean
|
||||||
|
*/
|
||||||
|
public function export($items, $fd=null) {
|
||||||
|
if (!$fd) $fd = fopen('php://output', 'w');
|
||||||
|
$mapping = $this -> _fields_mapping();
|
||||||
|
$headers = [];
|
||||||
|
foreach ($mapping as $field)
|
||||||
|
$headers[] = $field['label'];
|
||||||
|
$success = $this -> fputcsv($fd, array_values($headers));
|
||||||
|
foreach($items as $item) {
|
||||||
|
$row = [];
|
||||||
|
foreach ($mapping as $key => $info) {
|
||||||
|
$row[] = call_user_func(
|
||||||
|
$info['to_string'],
|
||||||
|
array_key_exists($key, $item)?$item[$key]:null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$success = $success && $this -> fputcsv($fd, $row);
|
||||||
|
}
|
||||||
|
return $success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load items
|
||||||
|
* @param resource $fd The file pointer where to load data
|
||||||
|
* @return array<int,array<string,mixed>>|false The loaded items or false in case of error
|
||||||
|
*/
|
||||||
|
public function load($fd=null) {
|
||||||
|
if (!$fd) $fd = fopen('php://stdin', 'r');
|
||||||
|
$mapping = $this -> _fields_mapping();
|
||||||
|
$rows_mapping = false;
|
||||||
|
$line = 0;
|
||||||
|
$items = [];
|
||||||
|
$error = false;
|
||||||
|
while (($row = $this -> fgetcsv($fd)) !== FALSE) {
|
||||||
|
$line++;
|
||||||
|
if ($rows_mapping === false) {
|
||||||
|
$rows_mapping = [];
|
||||||
|
foreach($row as $idx => $field) {
|
||||||
|
$map = false;
|
||||||
|
foreach($mapping as $map_field => $map_info) {
|
||||||
|
if ($map_info['label'] == $field) {
|
||||||
|
$map = $map_field;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($map) {
|
||||||
|
$rows_mapping[$idx] = $mapping[$map];
|
||||||
|
$rows_mapping[$idx]['name'] = $map;
|
||||||
|
unset($mapping[$map]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Log :: warning(
|
||||||
|
"No corresponding field found for column '%s' (#%d), ignore it.",
|
||||||
|
$field, $idx
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!$rows_mapping) {
|
||||||
|
Log :: warning("Invalid rows mapping loaded from line #%d", $line);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
Log :: debug(
|
||||||
|
"CSV :: load(): Rows mapping established from row #%d : %s",
|
||||||
|
$line,
|
||||||
|
implode(
|
||||||
|
', ',
|
||||||
|
array_map(
|
||||||
|
function($idx, $info) {
|
||||||
|
return sprintf("%s => %s", $idx, $info["label"]);
|
||||||
|
},
|
||||||
|
array_keys($rows_mapping),
|
||||||
|
array_values($rows_mapping)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$item = [];
|
||||||
|
foreach($rows_mapping as $idx => $field) {
|
||||||
|
$item[$field['name']] = call_user_func(
|
||||||
|
$field['from_string'],
|
||||||
|
$row[$idx]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Log :: trace("CSV :: load(): Item load from line #%d: %s", $line, implode_with_keys($item));
|
||||||
|
$items[] = $item;
|
||||||
|
}
|
||||||
|
catch (\Exception $e) {
|
||||||
|
Log :: error(
|
||||||
|
"Error occurred loading item from line #%d : %s\n%s",
|
||||||
|
$line,
|
||||||
|
$e->getMessage(),
|
||||||
|
print_r($row, true)
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log :: debug("CSV :: load(): %d item(s) loaded", count($items));
|
||||||
|
return $items;
|
||||||
|
}
|
||||||
|
}
|
98
src/Export/Generic.php
Normal file
98
src/Export/Generic.php
Normal file
|
@ -0,0 +1,98 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace EesyPHP\Export;
|
||||||
|
|
||||||
|
class Generic {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of fields in export. Could be an associative array to specify custom exporting
|
||||||
|
* parameters:
|
||||||
|
* [
|
||||||
|
* 'name',
|
||||||
|
* 'name' => 'label',
|
||||||
|
* 'name' => [
|
||||||
|
* 'label' => 'Name',
|
||||||
|
* // all other export specific stuff
|
||||||
|
* ],
|
||||||
|
* ].
|
||||||
|
* @var array<string,array>|array<string,string>|array<string>
|
||||||
|
*/
|
||||||
|
protected $fields;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of export parameters
|
||||||
|
* @var array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected $parameters;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array of export parameters default value
|
||||||
|
* @var array<string,mixed>
|
||||||
|
*/
|
||||||
|
protected static $default_parameters = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
* @param array<string,string>|array<string> $fields Array of the fields name in export
|
||||||
|
* @param array<string,mixed> $parameters Export parameters
|
||||||
|
*/
|
||||||
|
|
||||||
|
public function __construct($fields, $parameters=null) {
|
||||||
|
$this -> fields = $fields;
|
||||||
|
$this -> parameters = is_array($parameters)?$parameters:[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get parameter
|
||||||
|
* @param string $param Parameter name
|
||||||
|
* @param mixed $default Override parameter default value (optional)
|
||||||
|
* @return mixed Parameter value
|
||||||
|
*/
|
||||||
|
public function get_parameter($param, $default=null) {
|
||||||
|
$default = (
|
||||||
|
$default?
|
||||||
|
$default:
|
||||||
|
(
|
||||||
|
array_key_exists($param, static :: $default_parameters)?
|
||||||
|
static :: $default_parameters[$param]:
|
||||||
|
null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
array_key_exists($param, $this -> parameters)?
|
||||||
|
$this -> parameters[$param]:
|
||||||
|
$default
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow to accept parameters as properties
|
||||||
|
* @param string $key
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
public function __get($key) {
|
||||||
|
return $this -> get_parameter($key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute fields mapping info
|
||||||
|
* @return array<string,mixed> Fields mapping info
|
||||||
|
*/
|
||||||
|
protected function _fields_mapping() {
|
||||||
|
$map = [];
|
||||||
|
foreach($this -> fields as $key => $value) {
|
||||||
|
$key = is_int($key)?$value:$key;
|
||||||
|
$map[$key] = [
|
||||||
|
'label' => (
|
||||||
|
is_array($value) && array_key_exists('label', $value)?
|
||||||
|
$value['label']:strval($value)
|
||||||
|
),
|
||||||
|
];
|
||||||
|
// Keep all other import generic provided info and leave specific export type to handle them
|
||||||
|
if (is_array($value))
|
||||||
|
$map[$key] = array_merge($value, $map[$key]);
|
||||||
|
}
|
||||||
|
return $map;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue