Initial xentral_oss_20.3.c9ffacf

This commit is contained in:
Alex
2021-05-21 08:49:41 +02:00
parent 5406e4a551
commit 34e5ac43d9
18884 changed files with 2109867 additions and 0 deletions
@@ -0,0 +1,115 @@
<?php
namespace Xentral\Components\Exporter\Collection;
use ArrayObject;
use AppendIterator;
use Iterator;
use IteratorAggregate;
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
final class DataCollection implements Iterator
{
/** @var AppendIterator $data */
private $data;
/**
* @param array|Iterator $data
*
* @throws InvalidArgumentException
*/
public function __construct(...$data)
{
$this->data = new AppendIterator();
foreach ($data as $item) {
$this->append($item);
}
}
/**
* @param array|Iterator|IteratorAggregate $data
*
* @throws InvalidArgumentException
*/
public function append($data)
{
$type = gettype($data);
if ($type === 'object') {
$type = get_class($data);
if ($data instanceof Iterator) {
$type = 'Iterator';
}
if ($data instanceof IteratorAggregate) {
$type = 'IteratorAggregate';
}
}
switch ($type) {
case 'array':
$this->data->append((new ArrayObject($data))->getIterator());
break;
case 'Iterator':
$this->data->append($data);
break;
case 'IteratorAggregate':
$this->data->append($data->getIterator());
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
break;
}
}
/**
* Return the current element
*
* @return mixed Can return any type.
*/
public function current()
{
return $this->data->current();
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->data->next();
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->data->key();
}
/**
* Checks if current position is valid
*
* @return boolean Returns true on success or false on failure.
*/
public function valid()
{
return $this->data->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void
*/
public function rewind()
{
$this->data->rewind();
}
}
@@ -0,0 +1,120 @@
<?php
namespace Xentral\Components\Exporter\Collection;
use ArrayObject;
use Closure;
use Iterator;
use IteratorAggregate;
use Xentral\Components\Exporter\Exception\InvalidArgumentException;
use Xentral\Components\Exporter\Exception\InvalidReturnTypeException;
final class FormatterCollection implements Iterator
{
/** @var Iterator $data */
private $data;
/** @var callable $callback */
private $callback;
/**
* @param array|Iterator $data
* @param callable|Closure $callback
*
* @throws InvalidArgumentException
*/
public function __construct($data, $callback)
{
if (!is_callable($callback, false)) {
throw new InvalidArgumentException('Callback is not callable');
}
$this->callback = $callback;
$type = gettype($data);
if ($type === 'object') {
$type = get_class($data);
if ($data instanceof Iterator) {
$type = 'Iterator';
}
if ($data instanceof IteratorAggregate) {
$type = 'IteratorAggregate';
}
}
switch ($type) {
case 'array':
$this->data = (new ArrayObject($data))->getIterator();
break;
case 'Iterator':
$this->data = $data;
break;
case 'IteratorAggregate':
$this->data = $data->getIterator();
break;
default:
throw new InvalidArgumentException(sprintf('Unsupported type "%s".', $type));
break;
}
}
/**
* Return the current element
*
* @throws InvalidReturnTypeException
*
* @return mixed Can return any type.
*/
public function current()
{
$result = call_user_func($this->callback, $this->data->current());
if (!is_array($result)) {
throw new InvalidReturnTypeException('Formatter return type is invalid . Callable must return an array.');
}
return $result;
}
/**
* Move forward to next element
*
* @return void
*/
public function next()
{
$this->data->next();
}
/**
* Return the key of the current element
*
* @return mixed scalar on success, or null on failure.
*/
public function key()
{
return $this->data->key();
}
/**
* Checks if current position is valid
*
* @return boolean Returns true on success or false on failure.
*/
public function valid()
{
return $this->data->valid();
}
/**
* Rewind the Iterator to the first element
*
* @return void
*/
public function rewind()
{
$this->data->rewind();
}
}
@@ -0,0 +1,144 @@
<?php
namespace Xentral\Components\Exporter\Csv;
final class CsvConfig
{
/** @var string $delimiter */
private $delimiter;
/** @var string $enclosure */
private $enclosure;
/** @var string $escapeChar */
private $escapeChar;
/** @var string $sourceCharset */
private $sourceCharset;
/** @var string $targetCharset */
private $targetCharset;
/** @var bool $forceEnclosureEnabled */
private $forceEnclosureEnabled;
/**
* @param string $delimiter
* @param string $enclosure
* @param string $escapeChar
* @param string $targetCharset
* @param string $sourceCharset
* @param bool $forceEnclosureEnabled
*/
public function __construct(
$delimiter = ',',
$enclosure = '"',
$escapeChar = "\\",
$targetCharset = 'UTF-8',
$sourceCharset = 'UTF-8',
$forceEnclosureEnabled = false
) {
$this->delimiter = $delimiter;
$this->enclosure = $enclosure;
$this->escapeChar = $escapeChar;
$this->targetCharset = $targetCharset;
$this->sourceCharset = $sourceCharset;
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
}
/**
* @return string
*/
public function getDelimiter()
{
return $this->delimiter;
}
/**
* @return string
*/
public function getEnclosure()
{
return $this->enclosure;
}
/**
* @return string
*/
public function getEscapeChar()
{
return $this->escapeChar;
}
/**
* @return string
*/
public function getSourceCharset()
{
return $this->sourceCharset;
}
/**
* @return string
*/
public function getTargetCharset()
{
return $this->targetCharset;
}
/**
* @param string $delimiter
*/
public function setDelimiter($delimiter)
{
$this->delimiter = $delimiter;
}
/**
* @param string $enclosure
*/
public function setEnclosure($enclosure)
{
$this->enclosure = $enclosure;
}
/**
* @param string $escapeChar
*/
public function setEscapeChar($escapeChar)
{
$this->escapeChar = $escapeChar;
}
/**
* @param string $sourceCharset
*/
public function setSourceCharset($sourceCharset)
{
$this->sourceCharset = $sourceCharset;
}
/**
* @param string $targetCharset
*/
public function setTargetCharset($targetCharset)
{
$this->targetCharset = $targetCharset;
}
/**
* @return bool
*/
public function isForceEnclosureEnabled()
{
return $this->forceEnclosureEnabled;
}
/**
* @param bool $forceEnclosureEnabled
*/
public function setForceEnclosureEnabled($forceEnclosureEnabled)
{
$this->forceEnclosureEnabled = $forceEnclosureEnabled;
}
}
@@ -0,0 +1,70 @@
<?php
namespace Xentral\Components\Exporter\Csv;
use Iterator;
use Xentral\Components\Exporter\Exception\FileExistsException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
final class CsvExporter
{
/** @var CsvConfig $config */
private $config;
/**
* @param CsvConfig|null $config
*/
public function __construct(CsvConfig $config = null)
{
if ($config === null) {
$config = new CsvConfig();
}
$this->config = $config;
}
/**
* @param string $filePath Resource used for writing
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
*
* @throws FileExistsException|InvalidResourceException
*
* @return void
*/
public function export($filePath, $data)
{
$resource = $this->exportToResource($filePath, $data);
fclose($resource);
}
/**
* Same as ::export() beside that the created resource will be returned
*
* @param string $filePath Resource used for writing
* @param array|Iterator $data Multi-dimentional array, Generator or Iterator
*
* @throws FileExistsException|InvalidResourceException
*
* @return resource
*/
public function exportToResource($filePath, $data)
{
if (is_file($filePath)) {
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
}
// 'x+' = Create and open for reading and writing.
// File pointer will be placed at the beginning of the file.
// If the file already exists `fopen` will return false.
// 'b' = Enable binary mode
$resource = @fopen($filePath, 'x+b');
if ($resource === false) {
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
}
$writer = new CsvWriter($resource, $this->config);
$writer->writeLines($data);
return $resource;
}
}
@@ -0,0 +1,147 @@
<?php
namespace Xentral\Components\Exporter\Csv;
use Iterator;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
final class CsvWriter
{
/** @var resource $handle */
private $handle;
/** @var CsvConfig $config */
private $config;
/**
* @param resource $handle
* @param CsvConfig|null $config
*
* @throws InvalidResourceException If resource is not writable or invalid
* @throws PhpExtensionMissingException If mbstring is missing
*/
public function __construct($handle, CsvConfig $config = null)
{
if (!is_resource($handle)) {
throw new InvalidResourceException('First parameter is not a valid resource.');
}
if (!$this->isStreamWritable($handle)) {
throw new InvalidResourceException('Resource is not writable.');
}
if (!function_exists('mb_convert_encoding')) {
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
}
if ($config === null) {
$config = new CsvConfig();
}
$this->config = $config;
$this->handle = $handle;
}
/**
* @param array|Iterator $lines
*
* @return void
*/
public function writeLines($lines)
{
foreach ($lines as $line) {
$this->writeLine($line);
}
}
/**
* @param array $line
*
* @return void
*/
public function writeLine($line)
{
if ($this->config->isForceEnclosureEnabled()) {
$fields = $this->encloseAllValues($line);
fwrite(
$this->handle,
sprintf(
"%s\n",
implode($this->config->getDelimiter(), $this->convertCharset($fields))
)
);
return;
}
fputcsv(
$this->handle,
$this->convertCharset($line),
$this->config->getDelimiter(),
$this->config->getEnclosure(),
$this->config->getEscapeChar()
);
}
/***
* @param array $line
*
* @return array
*/
private function encloseAllValues($line)
{
$escapeChar = $this->config->getEscapeChar();
$enclosure = $this->config->getEnclosure();
$result = [];
foreach ($line as $key => $value) {
$value = str_replace($escapeChar . $enclosure, $enclosure, $value);
$value = str_replace($enclosure, $escapeChar . $enclosure, $value);
$result[$key] = sprintf('%1$s%2$s%1$s', $enclosure, $value);
}
return $result;
}
/**
* @param array $line
*
* @return array
*/
private function convertCharset($line)
{
if ($this->config->getSourceCharset() === $this->config->getTargetCharset()) {
return $line; // No conversion needed
}
$result = [];
foreach ($line as $key => $cellData) {
$result[$key] = mb_convert_encoding(
$cellData,
$this->config->getTargetCharset(),
$this->config->getSourceCharset()
);
}
return $result;
}
/**
* @param resource $handle
*
* @return bool
*/
private function isStreamWritable($handle)
{
$meta = stream_get_meta_data($handle);
$currentMode = $meta['mode'];
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
foreach ($writeModes as $writeMode) {
if (strpos($currentMode, $writeMode) !== false) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Xentral\Core\Exception\ComponentExceptionInterface;
interface ExporterExceptionInterface extends ComponentExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class FileExistsException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use InvalidArgumentException as SplInvalidArgumentException;
class InvalidArgumentException extends SplInvalidArgumentException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,62 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Exception;
class InvalidJsonException extends Exception implements ExporterExceptionInterface
{
/**
* @param int $errorCode
*
* @return self
*/
public static function fromJsonError($errorCode)
{
$exception = new self(self::mapJsonError($errorCode));
return $exception;
}
private static function mapJsonError($jsonError)
{
switch ($jsonError) {
case JSON_ERROR_NONE:
$msg = 'Unknown error';
break;
case JSON_ERROR_DEPTH:
$msg = 'The maximum stack depth has been exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = 'Invalid or malformed JSON';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = 'Control character error, possibly incorrectly encoded';
break;
case JSON_ERROR_SYNTAX:
$msg = 'Syntax error';
break;
case JSON_ERROR_UTF8:
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
case JSON_ERROR_RECURSION:
$msg = 'One or more recursive references in the value to be encoded';
break;
case JSON_ERROR_INF_OR_NAN:
$msg = 'One or more NAN or INF values in the value to be encoded';
break;
case JSON_ERROR_UNSUPPORTED_TYPE:
$msg = 'A value of a type that cannot be encoded was given';
break;
case JSON_ERROR_INVALID_PROPERTY_NAME:
$msg = 'A property name that cannot be encoded was given';
break;
case JSON_ERROR_UTF16:
$msg = 'Malformed UTF-16 characters, possibly incorrectly encoded';
break;
default:
$msg = 'Unknown Error';
}
return $msg;
}
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class InvalidResourceException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use LogicException;
class InvalidReturnTypeException extends LogicException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use RuntimeException;
class PhpExtensionMissingException extends RuntimeException implements ExporterExceptionInterface
{
}
@@ -0,0 +1,9 @@
<?php
namespace Xentral\Components\Exporter\Exception;
use Exception;
class ResourceWriteException extends Exception implements ExporterExceptionInterface
{
}
@@ -0,0 +1,27 @@
<?php
namespace Xentral\Components\Exporter\Json;
final class JsonConfig
{
/** @var int $options */
private $options;
/**
* @see https://www.php.net/manual/en/json.constants.php
*
* @param int $options
*/
public function __construct($options = 0)
{
$this->options = $options;
}
/**
* @return int
*/
public function getOptions()
{
return $this->options;
}
}
@@ -0,0 +1,69 @@
<?php
namespace Xentral\Components\Exporter\Json;
use Xentral\Components\Exporter\Exception\FileExistsException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
final class JsonExporter
{
/** @var JsonConfig $config */
private $config;
/**
* @param JsonConfig|null $config
*/
public function __construct(JsonConfig $config = null)
{
if ($config === null) {
$config = new JsonConfig();
}
$this->config = $config;
}
/**
* @param string $filePath Resource used for writing
* @param array $data Multi-dimentional array
*
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
* @return void
*/
public function export($filePath, $data)
{
$resource = $this->exportToResource($filePath, $data);
fclose($resource);
}
/**
* Same as ::export() beside that the created resource will be returned
*
* @param string $filePath Resource used for writing
* @param array $data Multi-dimentional array
*
* @throws \Xentral\Components\Exporter\Exception\InvalidJsonException
* @throws \Xentral\Components\Exporter\Exception\ResourceWriteException
* @return resource
*/
public function exportToResource($filePath, $data)
{
if (is_file($filePath)) {
throw new FileExistsException(sprintf('File creation failed. File "%s" already exists.', $filePath));
}
// 'x+' = Create and open for reading and writing.
// File pointer will be placed at the beginning of the file.
// If the file already exists `fopen` will return false.
// 'b' = Enable binary mode
$resource = @fopen($filePath, 'x+b');
if ($resource === false) {
throw new InvalidResourceException(sprintf('Failed to open resource for file path "%s".', $filePath));
}
$writer = new JsonWriter($resource, $this->config);
$writer->write($data);
return $resource;
}
}
@@ -0,0 +1,90 @@
<?php
namespace Xentral\Components\Exporter\Json;
use JsonSerializable;
use Xentral\Components\Exporter\Exception\InvalidJsonException;
use Xentral\Components\Exporter\Exception\InvalidResourceException;
use Xentral\Components\Exporter\Exception\PhpExtensionMissingException;
use Xentral\Components\Exporter\Exception\ResourceWriteException;
final class JsonWriter
{
/** @var resource $handle */
private $handle;
/** @var JsonConfig $config */
private $config;
/**
* @param resource $handle
* @param JsonConfig|null $config
*
* @throws InvalidResourceException If resource is not writable or invalid
* @throws PhpExtensionMissingException If mbstring is missing
*/
public function __construct($handle, JsonConfig $config = null)
{
if (!is_resource($handle)) {
throw new InvalidResourceException('First parameter is not a valid resource.');
}
if (!$this->isStreamWritable($handle)) {
throw new InvalidResourceException('Resource is not writable.');
}
if (!function_exists('mb_convert_encoding')) {
throw new PhpExtensionMissingException('Required PHP extension "mbstring" is missing.');
}
if ($config === null) {
$config = new JsonConfig();
}
$this->config = $config;
$this->handle = $handle;
}
/**
* @param array|JsonSerializable $data
*
* @throws PhpExtensionMissingException If json is missing
* @throws ResourceWriteException
* @throws InvalidJsonException
*/
public function write($data)
{
if (!function_exists('json_encode')) {
throw new PhpExtensionMissingException('Required PHP extension "json" is missing.');
}
$jsonOptions = $this->config->getOptions();
$jsonString = @json_encode($data, $jsonOptions);
if ($jsonString === false) {
throw InvalidJsonException::fromJsonError(json_last_error());
}
$writeResult = @fwrite($this->handle, $jsonString);
if ($writeResult === false) {
throw new ResourceWriteException("JSON could not be written to resource.");
}
}
/**
* @param resource $handle
*
* @return bool
*/
private function isStreamWritable($handle)
{
$meta = stream_get_meta_data($handle);
$currentMode = $meta['mode'];
$writeModes = ['r+', 'w', 'w+', 'a', 'a+', 'x', 'x+', 'c', 'c+'];
foreach ($writeModes as $writeMode) {
if (strpos($currentMode, $writeMode) !== false) {
return true;
}
}
return false;
}
}
@@ -0,0 +1,127 @@
# CSV-Exporter
## Datei erstellen
```php
<?php
use Xentral\Components\Database\Database;
use Xentral\Components\Exporter\Csv\CsvExporter;
/** @var Database $db */
$db = $container->get('Database');
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
$filePath = tempnam(sys_get_temp_dir(), 'employees') . '.csv';
$exporter = new CsvExporter();
$exporter->export($filePath, $data);
```
##### Rückgabewerte
* Die `export()` Methode liefert nichts zurück.
* Die `exportToResource()` gibt die geöffnete Ressource zurück. Die Ressource muss manuell (mit `fclose()`)
geschlossen werden. Der Dateizeiger ist auf `EOF` platziert.
##### Funktionsparameter
Beide `export*()`-Methoden nehmen die gleichen Parameter entgegen.
* Als erster Parameter wird ein absoluter Dateipfad erwartet. Alternativ kann ein Stream Wrapper angegeben werden.
Beispiele:
* `php://output` um die CSV direkt auszugeben (`echo`).
* `php://memory` um in den Arbeitsspeicher zu schreiben.
* `php://temp` um in eine temporäre Datei zu schreiben.
* Der zweite Parameter nimmt die Daten entgegen die als CSV exportiert werden sollen. Folgende Typen sind erlaubt:
* `array`
* `Generator`
* `Iterator`
* `IteratorAggregate`
##### Konstruktor-Parameter
* Der erste Konstruktor-Parameter ist optional und nimmt die CSV-Konfiguration entgegen;
siehe _Erweiterte Beispiele_ > _CSV konfigurieren_.
## Datei-Download erstellen
```php
<?php
use Xentral\Components\Database\Database;
use Xentral\Components\Exporter\Csv\CsvExporter;
/** @var Database $db */
$db = $container->get('Database');
$data = $db->yieldAll('SELECT e.* FROM employees AS e');
$exporter = new CsvExporter();
$resource = $exporter->exportToResource('php://memory', $data);
rewind($resource);
$stat = fstat($resource);
header('Cache-Control: must-revalidate');
header('Pragma: must-revalidate');
header('Content-type: text/csv');
header('Content-Disposition: attachment; filename="employees.csv"');
header('Content-Length: ' . $stat['size']);
fpassthru($resource);
fclose($resource);
```
## Erweiterte Beispiele
### CSV konfigurieren
```php
<?php
use Xentral\Components\Exporter\Csv\CsvConfig;
use Xentral\Components\Exporter\Csv\CsvExporter;
$csvConfig = new CsvConfig();
$csvConfig->setDelimiter(';');
$csvConfig->setEnclosure('"');
$csvConfig->setSourceCharset('UTF-8');
$csvConfig->setTargetCharset('ISO-8859-1');
$exporter = new CsvExporter($csvConfig);
$exporter->export($filePath, $data);
```
### Daten zusammenführen
```php
<?php
use Xentral\Components\Exporter\Csv\CsvExporter;
use Xentral\Components\Exporter\Collection\DataCollection;
$collection = new DataCollection($headline, $employees);
$exporter = new CsvExporter();
$exporter->export($filePath, $collection);
```
### Daten formatieren
```php
<?php
use Xentral\Components\Exporter\Csv\CsvExporter;
use Xentral\Components\Exporter\Collection\FormatterCollection;
$formatter = new FormatterCollection($data, function ($row) {
$row['fullname'] = $row['firstname'] . ' ' . $row['lastname'];
return $row;
});
$exporter = new CsvExporter();
$exporter->export($filePath, $formatter);
```
Statt einer anonymen Funktion (`Closure`) kann ein `callable` übergeben werden.