Add CSV export

This commit is contained in:
Benjamin Renard 2024-02-18 17:20:47 +01:00
parent 347de8eeaf
commit f9829d4aea
Signed by: bn8
GPG key ID: 3E2E1CE1907115BC
2 changed files with 338 additions and 0 deletions

240
src/Export/CSV.php Normal file
View 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
View 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;
}
}