diff --git a/composer.json b/composer.json index 8e3d1194..4f5891bc 100644 --- a/composer.json +++ b/composer.json @@ -117,9 +117,29 @@ "symfony/stopwatch": "7.3.*", "symfony/web-profiler-bundle": "7.3.*" }, - "suggest": { - "ext-bcmath": "Used to improve price calculation performance", - "ext-gmp": "Used to improve price calculation performanice" + "sort-packages": true, + "allow-plugins": { + "composer/package-versions-deprecated": true, + "symfony/flex": true, + "phpstan/extension-installer": true, + "symfony/runtime": true, + "php-http/discovery": true + } + }, + "autoload": { + "psr-4": { + "App\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "App\\Tests\\": "tests/" + } + }, + "scripts": { + "auto-scripts": { + "cache:clear": "symfony-cmd", + "assets:install %PUBLIC_DIR%": "symfony-cmd" }, "config": { "preferred-install": { @@ -170,4 +190,5 @@ "docker": true } } + } } diff --git a/composer.lock b/composer.lock index 6b9888d7..7acebb97 100644 --- a/composer.lock +++ b/composer.lock @@ -2500,6 +2500,85 @@ ], "time": "2022-01-17T14:14:24+00:00" }, + { + "name": "composer/pcre", + "version": "3.3.2", + "source": { + "type": "git", + "url": "https://github.com/composer/pcre.git", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<1.11.10" + }, + "require-dev": { + "phpstan/phpstan": "^1.12 || ^2", + "phpstan/phpstan-strict-rules": "^1 || ^2", + "phpunit/phpunit": "^8 || ^9" + }, + "type": "library", + "extra": { + "phpstan": { + "includes": [ + "extension.neon" + ] + }, + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Pcre\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + } + ], + "description": "PCRE wrapping library that offers type-safe preg_* replacements.", + "keywords": [ + "PCRE", + "preg", + "regex", + "regular expression" + ], + "support": { + "issues": "https://github.com/composer/pcre/issues", + "source": "https://github.com/composer/pcre/tree/3.3.2" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/composer/composer", + "type": "tidelift" + } + ], + "time": "2024-11-12T16:29:46+00:00" + }, { "name": "daverandom/libdns", "version": "v2.1.0", @@ -6514,6 +6593,190 @@ }, "time": "2023-07-31T13:36:50+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.1.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/6187e9cc4493da94b9b63eb2315821552015fca9", + "reference": "6187e9cc4493da94b9b63eb2315821552015fca9", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.1" + }, + "require-dev": { + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.16", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^10.0", + "vimeo/psalm": "^5.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.1.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2024-10-10T12:33:01+00:00" + }, + { + "name": "markbaker/complex", + "version": "3.0.2", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPComplex.git", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Complex\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@lange.demon.co.uk" + } + ], + "description": "PHP Class for working with complex numbers", + "homepage": "https://github.com/MarkBaker/PHPComplex", + "keywords": [ + "complex", + "mathematics" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPComplex/issues", + "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2" + }, + "time": "2022-12-06T16:21:08+00:00" + }, + { + "name": "markbaker/matrix", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/MarkBaker/PHPMatrix.git", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c", + "reference": "728434227fe21be27ff6d86621a1b13107a2562c", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-master", + "phpcompatibility/php-compatibility": "^9.3", + "phpdocumentor/phpdocumentor": "2.*", + "phploc/phploc": "^4.0", + "phpmd/phpmd": "2.*", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0", + "sebastian/phpcpd": "^4.0", + "squizlabs/php_codesniffer": "^3.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "Matrix\\": "classes/src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mark Baker", + "email": "mark@demon-angel.eu" + } + ], + "description": "PHP Class for working with matrices", + "homepage": "https://github.com/MarkBaker/PHPMatrix", + "keywords": [ + "mathematics", + "matrix", + "vector" + ], + "support": { + "issues": "https://github.com/MarkBaker/PHPMatrix/issues", + "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1" + }, + "time": "2022-12-02T22:17:43+00:00" + }, { "name": "masterminds/html5", "version": "2.10.0", @@ -8034,6 +8297,112 @@ }, "time": "2024-11-09T15:12:26+00:00" }, + { + "name": "phpoffice/phpspreadsheet", + "version": "4.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPOffice/PhpSpreadsheet.git", + "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/2ea9786632e6fac1aee601b6e426bcc723d8ce13", + "reference": "2ea9786632e6fac1aee601b6e426bcc723d8ce13", + "shasum": "" + }, + "require": { + "composer/pcre": "^1||^2||^3", + "ext-ctype": "*", + "ext-dom": "*", + "ext-fileinfo": "*", + "ext-gd": "*", + "ext-iconv": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-simplexml": "*", + "ext-xml": "*", + "ext-xmlreader": "*", + "ext-xmlwriter": "*", + "ext-zip": "*", + "ext-zlib": "*", + "maennchen/zipstream-php": "^2.1 || ^3.0", + "markbaker/complex": "^3.0", + "markbaker/matrix": "^3.0", + "php": "^8.1", + "psr/http-client": "^1.0", + "psr/http-factory": "^1.0", + "psr/simple-cache": "^1.0 || ^2.0 || ^3.0" + }, + "require-dev": { + "dealerdirect/phpcodesniffer-composer-installer": "dev-main", + "dompdf/dompdf": "^2.0 || ^3.0", + "friendsofphp/php-cs-fixer": "^3.2", + "mitoteam/jpgraph": "^10.3", + "mpdf/mpdf": "^8.1.1", + "phpcompatibility/php-compatibility": "^9.3", + "phpstan/phpstan": "^1.1 || ^2.0", + "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0", + "phpstan/phpstan-phpunit": "^1.0 || ^2.0", + "phpunit/phpunit": "^10.5", + "squizlabs/php_codesniffer": "^3.7", + "tecnickcom/tcpdf": "^6.5" + }, + "suggest": { + "dompdf/dompdf": "Option for rendering PDF with PDF Writer", + "ext-intl": "PHP Internationalization Functions", + "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers", + "mpdf/mpdf": "Option for rendering PDF with PDF Writer", + "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer" + }, + "type": "library", + "autoload": { + "psr-4": { + "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Maarten Balliauw", + "homepage": "https://blog.maartenballiauw.be" + }, + { + "name": "Mark Baker", + "homepage": "https://markbakeruk.net" + }, + { + "name": "Franck Lefevre", + "homepage": "https://rootslabs.net" + }, + { + "name": "Erik Tilt" + }, + { + "name": "Adrien Crivelli" + } + ], + "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine", + "homepage": "https://github.com/PHPOffice/PhpSpreadsheet", + "keywords": [ + "OpenXML", + "excel", + "gnumeric", + "ods", + "php", + "spreadsheet", + "xls", + "xlsx" + ], + "support": { + "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues", + "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/4.5.0" + }, + "time": "2025-07-24T05:15:59+00:00" + }, { "name": "phpstan/phpdoc-parser", "version": "2.3.0", @@ -20974,9 +21343,9 @@ "ext-json": "*", "ext-mbstring": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.2.0" }, - "plugin-api-version": "2.3.0" + "plugin-api-version": "2.6.0" } diff --git a/src/Form/AdminPages/ImportType.php b/src/Form/AdminPages/ImportType.php index 3e87812c..0bd3cea1 100644 --- a/src/Form/AdminPages/ImportType.php +++ b/src/Form/AdminPages/ImportType.php @@ -59,6 +59,8 @@ class ImportType extends AbstractType 'XML' => 'xml', 'CSV' => 'csv', 'YAML' => 'yaml', + 'XLSX' => 'xlsx', + 'XLS' => 'xls', ], 'label' => 'export.format', 'disabled' => $disabled, diff --git a/src/Services/ImportExportSystem/EntityExporter.php b/src/Services/ImportExportSystem/EntityExporter.php index 271642da..6c0cdd04 100644 --- a/src/Services/ImportExportSystem/EntityExporter.php +++ b/src/Services/ImportExportSystem/EntityExporter.php @@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Serializer\SerializerInterface; use function Symfony\Component\String\u; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; +use PhpOffice\PhpSpreadsheet\Writer\Xls; /** * Use this class to export an entity to multiple file formats. @@ -52,7 +55,7 @@ class EntityExporter protected function configureOptions(OptionsResolver $resolver): void { $resolver->setDefault('format', 'csv'); - $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']); + $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']); $resolver->setDefault('csv_delimiter', ';'); $resolver->setAllowedTypes('csv_delimiter', 'string'); @@ -88,6 +91,11 @@ class EntityExporter $options = $resolver->resolve($options); + //Handle Excel formats by converting from CSV + if (in_array($options['format'], ['xlsx', 'xls'])) { + return $this->exportToExcel($entities, $options); + } + //If include children is set, then we need to add the include_children group $groups = [$options['level']]; if ($options['include_children']) { @@ -122,6 +130,73 @@ class EntityExporter throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object)); } + /** + * Exports entities to Excel format (xlsx or xls). + * + * @param AbstractNamedDBElement[] $entities The entities to export + * @param array $options The export options + * + * @return string The Excel file content as binary string + */ + protected function exportToExcel(array $entities, array $options): string + { + //First get CSV data using existing serializer + $csvOptions = $options; + $csvOptions['format'] = 'csv'; + $groups = [$options['level']]; + if ($options['include_children']) { + $groups[] = 'include_children'; + } + + $csvData = $this->serializer->serialize($entities, 'csv', + [ + 'groups' => $groups, + 'as_collection' => true, + 'csv_delimiter' => $options['csv_delimiter'], + 'partdb_export' => true, + SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, + AbstractNormalizer::CIRCULAR_REFERENCE_HANDLER => $this->handleCircularReference(...), + ] + ); + + //Convert CSV to Excel + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + $rows = explode("\n", $csvData); + $rowIndex = 1; + + foreach ($rows as $row) { + if (trim($row) === '') { + continue; + } + + $columns = str_getcsv($row, $options['csv_delimiter']); + $colIndex = 1; + + foreach ($columns as $column) { + $cellCoordinate = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex) . $rowIndex; + $worksheet->setCellValue($cellCoordinate, $column); + $colIndex++; + } + $rowIndex++; + } + + //Save to memory stream + if ($options['format'] === 'xlsx') { + $writer = new Xlsx($spreadsheet); + } else { + $writer = new Xls($spreadsheet); + } + + ob_start(); + $writer->save('php://output'); + $content = ob_get_contents(); + ob_end_clean(); + + return $content; + } + /** * Exports an Entity or an array of entities to multiple file formats. * @@ -168,6 +243,12 @@ class EntityExporter case 'json': $content_type = 'application/json'; break; + case 'xlsx': + $content_type = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; + break; + case 'xls': + $content_type = 'application/vnd.ms-excel'; + break; } $response->headers->set('Content-Type', $content_type); diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index 11915cfb..a36dc2be 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\File\File; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Serializer\SerializerInterface; use Symfony\Component\Validator\Validator\ValidatorInterface; +use PhpOffice\PhpSpreadsheet\IOFactory; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use Psr\Log\LoggerInterface; /** * @see \App\Tests\Services\ImportExportSystem\EntityImporterTest @@ -50,7 +53,7 @@ class EntityImporter */ private const ENCODINGS = ["ASCII", "UTF-8", "ISO-8859-1", "ISO-8859-15", "Windows-1252", "UTF-16", "UTF-32"]; - public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator) + public function __construct(protected SerializerInterface $serializer, protected EntityManagerInterface $em, protected ValidatorInterface $validator, protected LoggerInterface $logger) { } @@ -102,7 +105,7 @@ class EntityImporter foreach ($names as $name) { //Count indentation level (whitespace characters at the beginning of the line) - $identSize = strlen($name)-strlen(ltrim($name)); + $identSize = strlen($name) - strlen(ltrim($name)); //If the line is intended more than the last line, we have a new parent element if ($identSize > end($indentations)) { @@ -195,16 +198,20 @@ class EntityImporter } //The [] behind class_name denotes that we expect an array. - $entities = $this->serializer->deserialize($data, $options['class'].'[]', $options['format'], + $entities = $this->serializer->deserialize( + $data, + $options['class'] . '[]', + $options['format'], [ 'groups' => $groups, 'csv_delimiter' => $options['csv_delimiter'], 'create_unknown_datastructures' => $options['create_unknown_datastructures'], 'path_delimiter' => $options['path_delimiter'], 'partdb_import' => true, - //Disable API Platform normalizer, as we don't want to use it here + //Disable API Platform normalizer, as we don't want to use it here SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true, - ]); + ] + ); //Ensure we have an array of entity elements. if (!is_array($entities)) { @@ -279,7 +286,7 @@ class EntityImporter 'path_delimiter' => '->', //The delimiter used to separate the path elements in the name of a structural element ]); - $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']); + $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml', 'xlsx', 'xls']); $resolver->setAllowedTypes('csv_delimiter', 'string'); $resolver->setAllowedTypes('preserve_children', 'bool'); $resolver->setAllowedTypes('class', 'string'); @@ -335,6 +342,33 @@ class EntityImporter */ public function importFile(File $file, array $options = [], array &$errors = []): array { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + $options = $resolver->resolve($options); + + if (in_array($options['format'], ['xlsx', 'xls'])) { + $this->logger->info('Converting Excel file to CSV', [ + 'filename' => $file->getFilename(), + 'format' => $options['format'], + 'delimiter' => $options['csv_delimiter'] + ]); + + $csvData = $this->convertExcelToCsv($file, $options['csv_delimiter']); + $options['format'] = 'csv'; + + $this->logger->debug('Excel to CSV conversion completed', [ + 'csv_length' => strlen($csvData), + 'csv_lines' => substr_count($csvData, "\n") + 1 + ]); + + // Log the converted CSV for debugging (first 1000 characters) + $this->logger->debug('Converted CSV preview', [ + 'csv_preview' => substr($csvData, 0, 1000) . (strlen($csvData) > 1000 ? '...' : '') + ]); + + return $this->importString($csvData, $options, $errors); + } + return $this->importString($file->getContent(), $options, $errors); } @@ -354,10 +388,103 @@ class EntityImporter 'xml' => 'xml', 'csv', 'tsv' => 'csv', 'yaml', 'yml' => 'yaml', + 'xlsx' => 'xlsx', + 'xls' => 'xls', default => null, }; } + /** + * Converts Excel file to CSV format using PhpSpreadsheet. + * + * @param File $file The Excel file to convert + * @param string $delimiter The CSV delimiter to use + * + * @return string The CSV data as string + */ + protected function convertExcelToCsv(File $file, string $delimiter = ';'): string + { + try { + $this->logger->debug('Loading Excel file', ['path' => $file->getPathname()]); + $spreadsheet = IOFactory::load($file->getPathname()); + $worksheet = $spreadsheet->getActiveSheet(); + + $csvData = []; + $highestRow = $worksheet->getHighestRow(); + $highestColumn = $worksheet->getHighestColumn(); + + $this->logger->debug('Excel file dimensions', [ + 'rows' => $highestRow, + 'columns_detected' => $highestColumn, + 'worksheet_title' => $worksheet->getTitle() + ]); + + $highestColumnIndex = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::columnIndexFromString($highestColumn); + + for ($row = 1; $row <= $highestRow; $row++) { + $rowData = []; + + // Read all columns using numeric index + for ($colIndex = 1; $colIndex <= $highestColumnIndex; $colIndex++) { + $col = \PhpOffice\PhpSpreadsheet\Cell\Coordinate::stringFromColumnIndex($colIndex); + try { + $cellValue = $worksheet->getCell("{$col}{$row}")->getCalculatedValue(); + $rowData[] = $cellValue ?? ''; + + } catch (\Exception $e) { + $this->logger->warning('Error reading cell value', [ + 'cell' => "{$col}{$row}", + 'error' => $e->getMessage() + ]); + $rowData[] = ''; + } + } + + $csvRow = implode($delimiter, array_map(function ($value) use ($delimiter) { + $value = (string) $value; + if (strpos($value, $delimiter) !== false || strpos($value, '"') !== false || strpos($value, "\n") !== false) { + return '"' . str_replace('"', '""', $value) . '"'; + } + return $value; + }, $rowData)); + + $csvData[] = $csvRow; + + // Log first few rows for debugging + if ($row <= 3) { + $this->logger->debug("Row {$row} converted", [ + 'original_data' => $rowData, + 'csv_row' => $csvRow, + 'first_cell_raw' => $worksheet->getCell("A{$row}")->getValue(), + 'first_cell_calculated' => $worksheet->getCell("A{$row}")->getCalculatedValue() + ]); + } + } + + $result = implode("\n", $csvData); + + $this->logger->info('Excel to CSV conversion successful', [ + 'total_rows' => count($csvData), + 'total_characters' => strlen($result) + ]); + + $this->logger->debug('Full CSV data', [ + 'csv_data' => $result + ]); + + return $result; + + } catch (\Exception $e) { + $this->logger->error('Failed to convert Excel to CSV', [ + 'file' => $file->getFilename(), + 'error' => $e->getMessage(), + 'trace' => $e->getTraceAsString() + ]); + throw $e; + } + } + + /** * This functions corrects the parent setting based on the children value of the parent. * diff --git a/tests/Services/ImportExportSystem/EntityExporterTest.php b/tests/Services/ImportExportSystem/EntityExporterTest.php index 004971ab..e9b924b1 100644 --- a/tests/Services/ImportExportSystem/EntityExporterTest.php +++ b/tests/Services/ImportExportSystem/EntityExporterTest.php @@ -26,6 +26,7 @@ use App\Entity\Parts\Category; use App\Services\ImportExportSystem\EntityExporter; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\HttpFoundation\Request; +use PhpOffice\PhpSpreadsheet\IOFactory; class EntityExporterTest extends WebTestCase { @@ -76,7 +77,40 @@ class EntityExporterTest extends WebTestCase $this->assertSame('application/json', $response->headers->get('Content-Type')); $this->assertNotEmpty($response->headers->get('Content-Disposition')); + } + public function testExportToExcel(): void + { + $entities = $this->getEntities(); + $xlsxData = $this->service->exportEntities($entities, ['format' => 'xlsx', 'level' => 'simple']); + $this->assertNotEmpty($xlsxData); + + $tempFile = tempnam(sys_get_temp_dir(), 'test_export') . '.xlsx'; + file_put_contents($tempFile, $xlsxData); + + $spreadsheet = IOFactory::load($tempFile); + $worksheet = $spreadsheet->getActiveSheet(); + + $this->assertSame('name', $worksheet->getCell('A1')->getValue()); + $this->assertSame('full_name', $worksheet->getCell('B1')->getValue()); + + $this->assertSame('Enitity 1', $worksheet->getCell('A2')->getValue()); + $this->assertSame('Enitity 1', $worksheet->getCell('B2')->getValue()); + + unlink($tempFile); + } + + public function testExportExcelFromRequest(): void + { + $entities = $this->getEntities(); + + $request = new Request(); + $request->request->set('format', 'xlsx'); + $request->request->set('level', 'simple'); + $response = $this->service->exportEntityFromRequest($entities, $request); + + $this->assertSame('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type')); + $this->assertStringContainsString('export_Category_simple.xlsx', $response->headers->get('Content-Disposition')); } } diff --git a/tests/Services/ImportExportSystem/EntityImporterTest.php b/tests/Services/ImportExportSystem/EntityImporterTest.php index fd5e8b9e..83367f80 100644 --- a/tests/Services/ImportExportSystem/EntityImporterTest.php +++ b/tests/Services/ImportExportSystem/EntityImporterTest.php @@ -36,6 +36,9 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Component\Validator\ConstraintViolation; use Symfony\Component\Validator\ConstraintViolationListInterface; +use Symfony\Component\HttpFoundation\File\File; +use PhpOffice\PhpSpreadsheet\Spreadsheet; +use PhpOffice\PhpSpreadsheet\Writer\Xlsx; #[Group('DB')] class EntityImporterTest extends WebTestCase @@ -207,6 +210,10 @@ EOT; yield ['json', 'json']; yield ['yaml', 'yml']; yield ['yaml', 'YAML']; + yield ['xlsx', 'xlsx']; + yield ['xlsx', 'XLSX']; + yield ['xls', 'xls']; + yield ['xls', 'XLS']; } #[DataProvider('formatDataProvider')] @@ -342,4 +349,41 @@ EOT; $this->assertSame($category, $results[0]->getCategory()); $this->assertSame('test,test2', $results[0]->getTags()); } + + public function testImportExcelFileProjects(): void + { + $spreadsheet = new Spreadsheet(); + $worksheet = $spreadsheet->getActiveSheet(); + + $worksheet->setCellValue('A1', 'name'); + $worksheet->setCellValue('B1', 'comment'); + $worksheet->setCellValue('A2', 'Test Excel 1'); + $worksheet->setCellValue('B2', 'Test Excel 1 notes'); + $worksheet->setCellValue('A3', 'Test Excel 2'); + $worksheet->setCellValue('B3', 'Test Excel 2 notes'); + + $tempFile = tempnam(sys_get_temp_dir(), 'test_excel') . '.xlsx'; + $writer = new Xlsx($spreadsheet); + $writer->save($tempFile); + + $file = new File($tempFile); + + $errors = []; + $results = $this->service->importFile($file, [ + 'class' => Project::class, + 'format' => 'xlsx', + 'csv_delimiter' => ';', + ], $errors); + + $this->assertCount(2, $results); + $this->assertEmpty($errors); + $this->assertContainsOnlyInstancesOf(Project::class, $results); + + $this->assertSame('Test Excel 1', $results[0]->getName()); + $this->assertSame('Test Excel 1 notes', $results[0]->getComment()); + $this->assertSame('Test Excel 2', $results[1]->getName()); + $this->assertSame('Test Excel 2 notes', $results[1]->getComment()); + + unlink($tempFile); + } }