From b1f5f462276781d47d5f299c1f3777c1ef2530c8 Mon Sep 17 00:00:00 2001 From: Simon Asika Date: Sat, 5 Feb 2022 22:23:01 +0800 Subject: [PATCH] First commit --- composer.json | 30 ++ composer.lock | 18 + src/Legacy/Excel/ExcelExporter.php | 565 +++++++++++++++++++++ src/Legacy/Excel/ExcelImporter.php | 497 ++++++++++++++++++ src/ToolkitPackage.php | 25 + src/Windwalker/Data/AbstractDTO.php | 145 ++++++ src/Windwalker/Data/DataTransferObject.php | 24 + 7 files changed, 1304 insertions(+) create mode 100644 composer.json create mode 100644 composer.lock create mode 100644 src/Legacy/Excel/ExcelExporter.php create mode 100644 src/Legacy/Excel/ExcelImporter.php create mode 100644 src/ToolkitPackage.php create mode 100644 src/Windwalker/Data/AbstractDTO.php create mode 100644 src/Windwalker/Data/DataTransferObject.php diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..1c19425 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "name": "lyrasoft/toolkit", + "description": "LYRASOFY development toolkit", + "type": "library", + "license": "MIT", + "autoload": { + "psr-4": { + "Lyrasoft\\Toolkit\\": "src/", + "Windwalker\\": "src/Windwalker/" + } + }, + "authors": [ + { + "name": "Simon Asika", + "email": "asika32764@gmail.com" + } + ], + "minimum-stability": "beta", + "require": {}, + "extra": { + "windwalker":{ + "packages": [ + "Lyrasoft\\Toolkit\\ToolkitPackage" + ], + "modules": [ + + ] + } + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..0d3d42b --- /dev/null +++ b/composer.lock @@ -0,0 +1,18 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "b2704281421b3e9434a92b05900b4991", + "packages": [], + "packages-dev": [], + "aliases": [], + "minimum-stability": "beta", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "2.1.0" +} diff --git a/src/Legacy/Excel/ExcelExporter.php b/src/Legacy/Excel/ExcelExporter.php new file mode 100644 index 0000000..883016c --- /dev/null +++ b/src/Legacy/Excel/ExcelExporter.php @@ -0,0 +1,565 @@ + 'foo.xlsx']); + * + * $exporter->addColumn('id', 'ID', [options]); + * $exporter->addColumn('title', 'Title', [options]); + * + * foreach ($items as $item) { + * $exporter->addRow(function (ExcelExporter $row) { + * $row->setRowCell('id', ...); + * $row->setRowCell('title', ...); + * }); + * } + * + * $exporter->download(); // Or render(): string + * ``` + * + * @since 1.5.13 + * + * @deprecated This is legacy class. + */ +class ExcelExporter +{ + use OptionAccessTrait; + + /** + * Property columns. + * + * @var array + */ + protected $columns = []; + + /** + * Property data. + * + * @var array + */ + protected $data = []; + + /** + * Property row. + * + * @var int + */ + protected $currentRow; + + /** + * ExcelExporter constructor. + * + * @param array $options + */ + public function __construct(array $options = []) + { + if (!class_exists(Spreadsheet::class)) { + throw new \DomainException('Please install phpoffice/phpspreadsheet first.'); + } + + $this->options = $options; + } + + /** + * addColumn + * + * Options: + * - width + * - number_format: https://phpspreadsheet.readthedocs.io/en/latest/topics/recipes/#styles + * - handler: callable($dimension, $code) + * + * @param string $id + * @param string $title + * @param array $options + * + * @return static + * + * @since 1.5.13 + */ + public function addColumn(string $id, string $title = '', array $options = []): self + { + $options['title'] = $title; + + $this->columns[$id] = $options; + + return $this; + } + + /** + * deleteRow + * + * @param string $id + * + * @return static + * + * @since 1.5.13 + */ + public function deleteColumn(string $id): self + { + unset($this->columns[$id]); + + return $this; + } + + /** + * Method to get property Columns + * + * @return array + * + * @since 1.5.13 + */ + public function getColumns(): array + { + return $this->columns; + } + + /** + * Method to set property columns + * + * @param array $columns + * + * @return static Return self to support chaining. + * + * @since 1.5.13 + */ + public function setColumns(array $columns): self + { + $this->columns = []; + + foreach ($columns as $name => $column) { + if (is_array($column)) { + $title = $column['title'] ?? ''; + $options = $column; + } else { + $title = $column; + $options = []; + } + + $this->addColumn($name, $title, $options); + } + + return $this; + } + + /** + * addRow + * + * @param callable|null $handler + * + * @return static + * + * @since 1.5.13 + */ + public function addRow(?callable $handler = null): self + { + $this->data[] = []; + + $this->currentRow = array_key_last($this->data); + + if ($handler) { + $handler($this); + } + + return $this; + } + + /** + * getRow + * + * @param int $rowId + * + * @return array|null + * + * @since 1.5.13 + */ + public function getRow(int $rowId): ?array + { + return $this->data[$rowId] ?? null; + } + + /** + * deleteRow + * + * @param int $rowId + * + * @return ExcelExporter + * + * @since 1.5.13 + */ + public function deleteRow(int $rowId): self + { + unset($this->data[$rowId]); + + if ($rowId === $this->currentRow) { + $this->currentRow = array_key_last($this->data); + } + + return $this; + } + + /** + * Method to get property CurrentRow + * + * @return int + * + * @since 1.5.13 + */ + public function getCurrentRow(): int + { + return $this->currentRow; + } + + /** + * Method to set property currentRow + * + * @param int $currentRow + * + * @return static Return self to support chaining. + * + * @since 1.5.13 + */ + public function setCurrentRow(int $currentRow): self + { + $this->currentRow = $currentRow; + + return $this; + } + + /** + * setRowData + * + * @param array $data + * @param int|null $rowId + * + * @return ExcelExporter + * + * @since 1.5.13 + */ + public function setRowData(array $data, ?int $rowId = null): self + { + $rowId = $rowId ?? $this->currentRow; + + $this->data[$rowId] = $data; + + return $this; + } + + /** + * setRowCell + * + * @param string $name + * @param mixed $value + * @param int|null $rowId + * + * @return ExcelExporter + * + * @since 1.5.13 + */ + public function setRowCell(string $name, $value, ?int $rowId = null): self + { + $rowId = $rowId ?? $this->currentRow; + $rowId = $rowId ?: 0; + + $this->data[$rowId][$name] = $value; + + return $this; + } + + /** + * Method to get property Data + * + * @return array + * + * @since 1.5.13 + */ + public function getData(): array + { + return $this->data; + } + + /** + * Method to set property data + * + * @param array $data + * + * @return static Return self to support chaining. + * + * @since 1.5.13 + */ + public function setData(array $data): self + { + $this->data = $data; + + return $this; + } + + /** + * render + * + * @param array $options + * @param string $format + * + * @return string + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @since 1.5.13 + */ + public function render(array $options = [], string $format = 'xlsx'): string + { + $file = WINDWALKER_TEMP . '/toolkit/excel/' . md5(uniqid('toolkit', true)) . '.' . $format; + + $this->save($file, $options, $format); + + $content = file_get_contents($file); + + Filesystem::delete($file); + + return $content; + } + + /** + * save + * + * @param string $file + * @param array $options + * @param string $format + * + * @return void + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @since 1.5.13 + */ + public function save(string $file, array $options = [], string $format = 'xlsx'): void + { + if (!str_contains($file, 'php://')) { + Filesystem::mkdir(dirname($file)); + } + + $this->prepareExcelWriter($options, $format)->save($file); + } + + public function printHtmlTable(string $dest = 'php://output', array $options = []): void + { + $this->save($dest, $options, 'Html'); + die; + } + + /** + * download + * + * @param string $filename + * @param array $options + * @param string $format + * + * @return void + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * @throws \Exception + * @since 1.5.13 + */ + public function download(?string $filename = null, array $options = [], string $format = 'xlsx'): void + { + // DebuggerHelper::disableConsole(); + + if (!$filename && $this->getOption('title')) { + $filename = $this->getOption('title') . '.' . $format; + } + + if (!$filename) { + $filename = 'Export-' . Chronos::now('Y-m-d-H-i-s') . '.' . $format; + } + + // Redirect output to a client’s web browser (Xlsx) + $response = HeaderHelper::prepareAttachmentHeaders(new Response(), $filename); + + (new Output())->sendHeaders($response); + + $this->save('php://output', $options, $format); + die; + } + + /** + * prepareExcelWriter + * + * @param array $options + * @param string $format + * + * @return IWriter + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @throws \PhpOffice\PhpSpreadsheet\Writer\Exception + * + * @since 1.5.13 + */ + protected function prepareExcelWriter(array $options = [], string $format = 'xlsx'): IWriter + { + $spreadsheet = $this->getSpreadsheet($options); + + return IOFactory::createWriter($spreadsheet, ucfirst($format)); + } + + /** + * prepareSpreadsheet + * + * @param Spreadsheet|null $spreadsheet + * + * @return Spreadsheet + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @since 1.5.18 + */ + public function getSpreadsheet(array $options = [], ?Spreadsheet $spreadsheet = null): Spreadsheet + { + $spreadsheet = $spreadsheet ?? new Spreadsheet(); + + $creator = (string) $this->getOption('creator'); + $title = (string) $this->getOption('title'); + $desc = (string) $this->getOption('description'); + + $properties = $spreadsheet->getProperties(); + + if ($creator !== '') { + $properties->setCreator($this->getOption('creator', 'Windwalker')); + } + + if ($title !== '') { + $properties->setTitle($title) + ->setSubject($title); + } + + if ($desc !== '') { + $properties->setDescription($desc); + } + + $preprocess = $options['preprocess'] ?? null; + + if ($preprocess) { + $preprocess($spreadsheet); + } + + $sheet = $spreadsheet->getActiveSheet(); + + $dataset = []; + + if (Arr::get($options, 'show_header') ?? true) { + $dataset[] = array_column($this->columns, 'title'); + } + + foreach ($this->data as $datum) { + $row = []; + + foreach ($this->columns as $id => $column) { + $row[] = $datum[$id] ?? ''; + } + + $dataset[] = $row; + } + + foreach (array_values($this->columns) as $i => $column) { + $code = static::num2alpha($i); + + $style = $sheet->getStyle($code . ':' . $code); + $dimension = $sheet->getColumnDimension($code); + + if ($column['width'] ?? null) { + $dimension->setWidth($column['width']); + } + + if ($column['number_format'] ?? null) { + $style->getNumberFormat()->setFormatCode($column['number_format']); + } + + if (is_callable($column['handler'] ?? null)) { + $column['handler']($dimension, $code); + } + } + + $sheet->fromArray($dataset); + + $postprocess = $options['postprocess'] ?? null; + + if ($postprocess) { + $postprocess($spreadsheet); + } + + return $spreadsheet; + } + + /** + * num2alpha + * + * @see https://stackoverflow.com/a/5554413 + * + * @param int $n + * + * @return string + * + * @since 1.5.13 + */ + public static function num2alpha(int $n): string + { + for ($r = ''; $n >= 0; $n = (int) ($n / 26) - 1) { + $r = chr($n % 26 + 0x41) . $r; + } + + return $r; + } + + /** + * alpha2num + * + * @see https://stackoverflow.com/a/5554413 + * + * @param int $a + * + * @return string + * + * @since 1.5.13 + */ + public static function alpha2num(int $a): string + { + $l = strlen($a); + $n = 0; + + for ($i = 0; $i < $l; $i++) { + $n = $n * 26 + ord($a[$i]) - 0x40; + } + + return $n - 1; + } +} diff --git a/src/Legacy/Excel/ExcelImporter.php b/src/Legacy/Excel/ExcelImporter.php new file mode 100644 index 0000000..2781a66 --- /dev/null +++ b/src/Legacy/Excel/ExcelImporter.php @@ -0,0 +1,497 @@ +loadFile($file, $format); + } elseif ($file instanceof \SplFileInfo) { + $this->loadFile($file, $format); + } else { + $this->load($file, $format); + } + } + } + + /** + * loadFile + * + * @param string|\SplFileInfo $file + * @param string|null $format + * + * @return ExcelImporter + * + * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception + * @since 1.5.13 + */ + public function loadFile(string|\SplFileInfo $file, ?string $format = null): self + { + $file = FileObject::unwrap($file); + + $format = $format ?? Path::getExtension($file); + + $reader = $this->createReader($format); + + $this->spreadsheet = $reader->load($file); + + return $this; + } + + /** + * getSheetIterator + * + * @param bool $asValue + * @param int|string|null $sheet + * + * @return \Generator + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @since 1.5.13 + */ + public function getRowIterator(bool $asValue = false, mixed $sheet = null): \Generator + { + if (is_int($sheet)) { + $worksheet = $this->spreadsheet->getSheet($sheet); + } elseif (is_string($sheet)) { + $worksheet = $this->spreadsheet->getSheetByName($sheet); + } else { + $worksheet = $this->spreadsheet->getActiveSheet(); + } + + return $this->iterateSheetRows($worksheet, $asValue); + } + + /** + * getColumnIterator + * + * @param bool $asValue + * @param int|string|null $sheet + * + * @return \Generator + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @since 1.5.14 + */ + public function getColumnIterator(bool $asValue = false, $sheet = null): \Generator + { + if (is_int($sheet)) { + $worksheet = $this->spreadsheet->getSheet($sheet); + } elseif (is_string($sheet)) { + $worksheet = $this->spreadsheet->getSheetByName($sheet); + } else { + $worksheet = $this->spreadsheet->getActiveSheet(); + } + + return $this->iterateSheetColumns($worksheet, $asValue); + } + + /** + * getSheetsIterator + * + * @param bool $asValue + * + * @return \Generator + * + * @since 1.5.13 + */ + public function getSheetsIterator(bool $asValue = false): ?\Generator + { + $loop = function () use ($asValue) { + $sheets = $this->spreadsheet->getAllSheets(); + + foreach ($sheets as $sheet) { + yield $sheet->getTitle() => $this->iterateSheetRows($sheet, $asValue); + } + }; + + return $loop(); + } + + /** + * getSheetData + * + * @param int|string|null $sheet + * + * @return array + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @since 1.5.13 + */ + public function getSheetData($sheet = null): array + { + return iterator_to_array($this->getRowIterator(true, $sheet)); + } + + /** + * getAlldata + * + * @return array + * + * @since 1.5.13 + */ + public function getAllData(): array + { + return array_map( + 'iterator_to_array', + iterator_to_array($this->getSheetsIterator(true)) + ); + } + + /** + * iterateSheet + * + * @param Worksheet $sheet + * @param bool $asValue + * + * @return \Generator|null + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * @since 1.5.13 + */ + protected function iterateSheetRows(Worksheet $sheet, bool $asValue = false): ?\Generator + { + $fields = []; + + foreach ($sheet->getRowIterator() as $i => $row) { + if ($i < $this->startFrom) { + continue; + } + + // First row + if ($i === $this->startFrom && $this->headerAsField) { + // Prepare fields title + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + + foreach ($cellIterator as $cell) { + $fields[$cell->getColumn()] = $col = $cell->getFormattedValue(); + + if ($col === '') { + $fields[$cell->getColumn()] = $cell->getColumn(); + } + } + + continue; + } + + $item = []; + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + + foreach ($cellIterator as $cell) { + $column = $this->headerAsField + ? $fields[$cell->getColumn()] + : $cell->getColumn(); + + if ($asValue) { + $item[$column] = $cell->getFormattedValue(); + } else { + $item[$column] = $cell; + } + } + + yield $i => $item; + } + } + + /** + * iterateSheetColumns + * + * @param Worksheet $sheet + * @param bool $asValue + * + * @return \Generator|null + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @since 1.5.14 + */ + protected function iterateSheetColumns(Worksheet $sheet, bool $asValue = false): ?\Generator + { + $fields = []; + + foreach ($sheet->getColumnIterator() as $f => $row) { + $i = Coordinate::columnIndexFromString($f); + + if ($i < $this->startFrom) { + continue; + } + + // First row + if ($i === $this->startFrom && $this->headerAsField) { + // Prepare fields title + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + + foreach ($cellIterator as $cell) { + $fields[$cell->getRow()] = $col = $cell->getFormattedValue(); + + if ($col === '') { + $fields[$cell->getRow()] = $cell->getRow(); + } + } + } + + $item = []; + $cellIterator = $row->getCellIterator(); + $cellIterator->setIterateOnlyExistingCells(false); + + foreach ($cellIterator as $cell) { + $column = $this->headerAsField + ? $fields[$cell->getRow()] + : $cell->getRow(); + + if ($asValue) { + $item[$column] = $cell->getFormattedValue(); + } else { + $item[$column] = $cell; + } + } + + yield $f => $item; + } + } + + /** + * eachSheet + * + * @param callable $handler + * @param bool $asValue + * @param int|string|null $sheet + * + * @return void + * + * @throws \PhpOffice\PhpSpreadsheet\Exception + * + * @since 1.5.13 + */ + public function eachSheet(callable $handler, bool $asValue = false, $sheet = null): void + { + foreach ($this->getRowIterator($asValue, $sheet) as $key => $item) { + $handler($item, $key); + } + } + + /** + * eachAll + * + * @param callable $handler + * @param bool $asValue + * + * @return void + * + * @since 1.5.13 + */ + public function eachAll(callable $handler, bool $asValue = false): void + { + /** @var \Generator $sheet */ + foreach ($this->getSheetsIterator($asValue) as $sheet) { + foreach ($sheet as $key => $item) { + $handler($item, $key, $sheet); + } + } + } + + /** + * load + * + * @param string $data + * @param string $format + * + * @return ExcelImporter + * + * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception + * + * @since 1.5.13 + */ + public function load(string|StreamInterface $data, string $format = 'Xlsx'): self + { + $temp = Filesystem::createTemp(WINDWALKER_TEMP); + + $temp->write($data); + + $this->loadFile($temp, $format); + + register_shutdown_function(fn () => $temp->delete()); + + return $this; + } + + /** + * createReader + * + * @param string|null $format + * + * @return IReader + * + * @throws \PhpOffice\PhpSpreadsheet\Reader\Exception + * + * @since 1.5.13 + */ + public function createReader(string $format = null): IReader + { + if (!class_exists(Spreadsheet::class)) { + throw new \DomainException('Please install phpoffice/phpspreadsheet first.'); + } + + $format = $format ?? 'xlsx'; + + $reader = IOFactory::createReader(ucfirst($format)); + $reader->setReadDataOnly(true); + + return $reader; + } + + /** + * Method to set property ignoreHeader + * + * @param int $ignoreHeader + * + * @return static Return self to support chaining. + * + * @since 1.5.13 + * + * @deprecated + */ + public function ignoreHeader(bool $ignoreHeader): self + { + $this->startFrom = (int) $ignoreHeader; + + return $this; + } + + /** + * startFromRow + * + * @param int $start + * + * @return static + * + * @since 1.5.14 + */ + public function startFrom(int $start): self + { + $this->startFrom = $start; + + return $this; + } + + /** + * Method to set property headerAsField + * + * @param bool $headerAsField + * + * @return static Return self to support chaining. + * + * @since 1.5.13 + */ + public function headerAsField(bool $headerAsField) + { + $this->headerAsField = $headerAsField; + + return $this; + } + + /** + * Retrieve an external iterator + * @link https://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing Iterator or + * Traversable + * @since 5.0.0 + * @throws \PhpOffice\PhpSpreadsheet\Exception + */ + public function getIterator(): Traversable + { + return $this->getRowIterator(true); + } + + /** + * Method to get property Spreadsheet + * + * @return Spreadsheet + * + * @since 1.5.14 + */ + public function getSpreadsheet(): Spreadsheet + { + return $this->spreadsheet; + } + + /** + * Method to set property spreadsheet + * + * @param Spreadsheet $spreadsheet + * + * @return static Return self to support chaining. + * + * @since 1.5.14 + */ + public function setSpreadsheet(Spreadsheet $spreadsheet): self + { + $this->spreadsheet = $spreadsheet; + + return $this; + } +} diff --git a/src/ToolkitPackage.php b/src/ToolkitPackage.php new file mode 100644 index 0000000..ae9af8f --- /dev/null +++ b/src/ToolkitPackage.php @@ -0,0 +1,25 @@ +data = $item; + + if ($keepFields !== null) { + $this->keepFields = $keepFields; + } + + $this->configure($item); + } + + abstract protected function configure(object $data): void; + + /** + * @return object|T + */ + public function getData(): object + { + return $this->data; + } + + /** + * @param object $data + * + * @return static Return self to support chaining. + */ + public function setData(object $data): static + { + $this->data = $data; + + return $this; + } + + public function __call(string $name, array $arguments) + { + return $this->data->$name(...$arguments); + } + + public function __get(string $name): mixed + { + return $this->data->$name; + } + + public function __set(string $name, $value): void + { + $this->data->$name = $value; + } + + public function __isset(string $name): bool + { + return isset($this->data[$name]); + } + + public function __unset(string $name): void + { + unset($this->data[$name]); + } + + public function getKeepFields(): array + { + return $this->keepFields; + } + + public function addKeepFields(string ...$fields): static + { + $this->keepFields = array_merge( + $this->keepFields, + $fields + ); + + return $this; + } + + /** + * @param array $keepFields + * + * @return static Return self to support chaining. + */ + public function setKeepFields(array $keepFields): static + { + $this->keepFields = $keepFields; + + return $this; + } + + public function dump(bool $recursive = false, bool $onlyDumpable = false): array + { + return TypeCast::toArray($this->data, $recursive, $onlyDumpable); + } + + public function jsonSerialize(): array + { + return Arr::only( + $this->dump(), + $this->getKeepFields() + ); + } + + public function extract(ORM $orm): array + { + return $orm->extractEntity($this->data); + } +} diff --git a/src/Windwalker/Data/DataTransferObject.php b/src/Windwalker/Data/DataTransferObject.php new file mode 100644 index 0000000..aff83f7 --- /dev/null +++ b/src/Windwalker/Data/DataTransferObject.php @@ -0,0 +1,24 @@ +