diff --git a/src/Export/CSV.php b/src/Export/CSV.php new file mode 100644 index 0000000..2fa8f84 --- /dev/null +++ b/src/Export/CSV.php @@ -0,0 +1,240 @@ + 'label', + * 'name' => [ + * 'label' => 'Name', + * 'to_string' => [callable], + * 'from_string' => [callable], + * ], + * ]. + * @var array|array + */ + protected $fields; + + /** + * Array of export parameters default value + * @var array + */ + 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 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 $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|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> $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>|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; + } +} diff --git a/src/Export/Generic.php b/src/Export/Generic.php new file mode 100644 index 0000000..db1d87e --- /dev/null +++ b/src/Export/Generic.php @@ -0,0 +1,98 @@ + 'label', + * 'name' => [ + * 'label' => 'Name', + * // all other export specific stuff + * ], + * ]. + * @var array|array|array + */ + protected $fields; + + /** + * Array of export parameters + * @var array + */ + protected $parameters; + + /** + * Array of export parameters default value + * @var array + */ + protected static $default_parameters = []; + + /** + * Constructor + * @param array|array $fields Array of the fields name in export + * @param array $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 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; + } + +}