mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 02:59:29 +00:00
Implement excel based import/export
This commit is contained in:
parent
c5751b2aa6
commit
facfb37383
7 changed files with 690 additions and 12 deletions
|
|
@ -117,9 +117,29 @@
|
||||||
"symfony/stopwatch": "7.3.*",
|
"symfony/stopwatch": "7.3.*",
|
||||||
"symfony/web-profiler-bundle": "7.3.*"
|
"symfony/web-profiler-bundle": "7.3.*"
|
||||||
},
|
},
|
||||||
"suggest": {
|
"sort-packages": true,
|
||||||
"ext-bcmath": "Used to improve price calculation performance",
|
"allow-plugins": {
|
||||||
"ext-gmp": "Used to improve price calculation performanice"
|
"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": {
|
"config": {
|
||||||
"preferred-install": {
|
"preferred-install": {
|
||||||
|
|
@ -170,4 +190,5 @@
|
||||||
"docker": true
|
"docker": true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
373
composer.lock
generated
373
composer.lock
generated
|
|
@ -2500,6 +2500,85 @@
|
||||||
],
|
],
|
||||||
"time": "2022-01-17T14:14:24+00:00"
|
"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",
|
"name": "daverandom/libdns",
|
||||||
"version": "v2.1.0",
|
"version": "v2.1.0",
|
||||||
|
|
@ -6514,6 +6593,190 @@
|
||||||
},
|
},
|
||||||
"time": "2023-07-31T13:36:50+00:00"
|
"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",
|
"name": "masterminds/html5",
|
||||||
"version": "2.10.0",
|
"version": "2.10.0",
|
||||||
|
|
@ -8034,6 +8297,112 @@
|
||||||
},
|
},
|
||||||
"time": "2024-11-09T15:12:26+00:00"
|
"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",
|
"name": "phpstan/phpdoc-parser",
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
|
|
@ -20974,9 +21343,9 @@
|
||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-mbstring": "*"
|
"ext-mbstring": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"platform-overrides": {
|
"platform-overrides": {
|
||||||
"php": "8.2.0"
|
"php": "8.2.0"
|
||||||
},
|
},
|
||||||
"plugin-api-version": "2.3.0"
|
"plugin-api-version": "2.6.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -59,6 +59,8 @@ class ImportType extends AbstractType
|
||||||
'XML' => 'xml',
|
'XML' => 'xml',
|
||||||
'CSV' => 'csv',
|
'CSV' => 'csv',
|
||||||
'YAML' => 'yaml',
|
'YAML' => 'yaml',
|
||||||
|
'XLSX' => 'xlsx',
|
||||||
|
'XLS' => 'xls',
|
||||||
],
|
],
|
||||||
'label' => 'export.format',
|
'label' => 'export.format',
|
||||||
'disabled' => $disabled,
|
'disabled' => $disabled,
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\Response;
|
||||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use function Symfony\Component\String\u;
|
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.
|
* Use this class to export an entity to multiple file formats.
|
||||||
|
|
@ -52,7 +55,7 @@ class EntityExporter
|
||||||
protected function configureOptions(OptionsResolver $resolver): void
|
protected function configureOptions(OptionsResolver $resolver): void
|
||||||
{
|
{
|
||||||
$resolver->setDefault('format', 'csv');
|
$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->setDefault('csv_delimiter', ';');
|
||||||
$resolver->setAllowedTypes('csv_delimiter', 'string');
|
$resolver->setAllowedTypes('csv_delimiter', 'string');
|
||||||
|
|
@ -88,6 +91,11 @@ class EntityExporter
|
||||||
|
|
||||||
$options = $resolver->resolve($options);
|
$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
|
//If include children is set, then we need to add the include_children group
|
||||||
$groups = [$options['level']];
|
$groups = [$options['level']];
|
||||||
if ($options['include_children']) {
|
if ($options['include_children']) {
|
||||||
|
|
@ -122,6 +130,73 @@ class EntityExporter
|
||||||
throw new CircularReferenceException('Circular reference detected for object of type '.get_class($object));
|
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.
|
* Exports an Entity or an array of entities to multiple file formats.
|
||||||
*
|
*
|
||||||
|
|
@ -168,6 +243,12 @@ class EntityExporter
|
||||||
case 'json':
|
case 'json':
|
||||||
$content_type = 'application/json';
|
$content_type = 'application/json';
|
||||||
break;
|
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);
|
$response->headers->set('Content-Type', $content_type);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,9 @@ use Symfony\Component\HttpFoundation\File\File;
|
||||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
use Symfony\Component\Serializer\SerializerInterface;
|
use Symfony\Component\Serializer\SerializerInterface;
|
||||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use Psr\Log\LoggerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see \App\Tests\Services\ImportExportSystem\EntityImporterTest
|
* @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"];
|
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) {
|
foreach ($names as $name) {
|
||||||
//Count indentation level (whitespace characters at the beginning of the line)
|
//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 the line is intended more than the last line, we have a new parent element
|
||||||
if ($identSize > end($indentations)) {
|
if ($identSize > end($indentations)) {
|
||||||
|
|
@ -195,16 +198,20 @@ class EntityImporter
|
||||||
}
|
}
|
||||||
|
|
||||||
//The [] behind class_name denotes that we expect an array.
|
//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,
|
'groups' => $groups,
|
||||||
'csv_delimiter' => $options['csv_delimiter'],
|
'csv_delimiter' => $options['csv_delimiter'],
|
||||||
'create_unknown_datastructures' => $options['create_unknown_datastructures'],
|
'create_unknown_datastructures' => $options['create_unknown_datastructures'],
|
||||||
'path_delimiter' => $options['path_delimiter'],
|
'path_delimiter' => $options['path_delimiter'],
|
||||||
'partdb_import' => true,
|
'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,
|
SkippableItemNormalizer::DISABLE_ITEM_NORMALIZER => true,
|
||||||
]);
|
]
|
||||||
|
);
|
||||||
|
|
||||||
//Ensure we have an array of entity elements.
|
//Ensure we have an array of entity elements.
|
||||||
if (!is_array($entities)) {
|
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
|
'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('csv_delimiter', 'string');
|
||||||
$resolver->setAllowedTypes('preserve_children', 'bool');
|
$resolver->setAllowedTypes('preserve_children', 'bool');
|
||||||
$resolver->setAllowedTypes('class', 'string');
|
$resolver->setAllowedTypes('class', 'string');
|
||||||
|
|
@ -335,6 +342,33 @@ class EntityImporter
|
||||||
*/
|
*/
|
||||||
public function importFile(File $file, array $options = [], array &$errors = []): array
|
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);
|
return $this->importString($file->getContent(), $options, $errors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -354,10 +388,103 @@ class EntityImporter
|
||||||
'xml' => 'xml',
|
'xml' => 'xml',
|
||||||
'csv', 'tsv' => 'csv',
|
'csv', 'tsv' => 'csv',
|
||||||
'yaml', 'yml' => 'yaml',
|
'yaml', 'yml' => 'yaml',
|
||||||
|
'xlsx' => 'xlsx',
|
||||||
|
'xls' => 'xls',
|
||||||
default => null,
|
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.
|
* This functions corrects the parent setting based on the children value of the parent.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ use App\Entity\Parts\Category;
|
||||||
use App\Services\ImportExportSystem\EntityExporter;
|
use App\Services\ImportExportSystem\EntityExporter;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
|
||||||
class EntityExporterTest extends WebTestCase
|
class EntityExporterTest extends WebTestCase
|
||||||
{
|
{
|
||||||
|
|
@ -76,7 +77,40 @@ class EntityExporterTest extends WebTestCase
|
||||||
|
|
||||||
$this->assertSame('application/json', $response->headers->get('Content-Type'));
|
$this->assertSame('application/json', $response->headers->get('Content-Type'));
|
||||||
$this->assertNotEmpty($response->headers->get('Content-Disposition'));
|
$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'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,9 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||||
use Symfony\Component\Validator\ConstraintViolation;
|
use Symfony\Component\Validator\ConstraintViolation;
|
||||||
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
use Symfony\Component\Validator\ConstraintViolationListInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\File\File;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Writer\Xlsx;
|
||||||
|
|
||||||
#[Group('DB')]
|
#[Group('DB')]
|
||||||
class EntityImporterTest extends WebTestCase
|
class EntityImporterTest extends WebTestCase
|
||||||
|
|
@ -207,6 +210,10 @@ EOT;
|
||||||
yield ['json', 'json'];
|
yield ['json', 'json'];
|
||||||
yield ['yaml', 'yml'];
|
yield ['yaml', 'yml'];
|
||||||
yield ['yaml', 'YAML'];
|
yield ['yaml', 'YAML'];
|
||||||
|
yield ['xlsx', 'xlsx'];
|
||||||
|
yield ['xlsx', 'XLSX'];
|
||||||
|
yield ['xls', 'xls'];
|
||||||
|
yield ['xls', 'XLS'];
|
||||||
}
|
}
|
||||||
|
|
||||||
#[DataProvider('formatDataProvider')]
|
#[DataProvider('formatDataProvider')]
|
||||||
|
|
@ -342,4 +349,41 @@ EOT;
|
||||||
$this->assertSame($category, $results[0]->getCategory());
|
$this->assertSame($category, $results[0]->getCategory());
|
||||||
$this->assertSame('test,test2', $results[0]->getTags());
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue