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