Implement excel based import/export

This commit is contained in:
barisgit 2025-08-01 19:32:49 +02:00
parent 22590913c5
commit 3ee2d3fe5d
7 changed files with 833 additions and 175 deletions

View file

@ -1,170 +1,171 @@
{ {
"name": "part-db/part-db-server", "name": "part-db/part-db-server",
"type": "project", "type": "project",
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"require": { "require": {
"php": "^8.1", "php": "^8.1",
"ext-ctype": "*", "ext-ctype": "*",
"ext-dom": "*", "ext-dom": "*",
"ext-gd": "*", "ext-gd": "*",
"ext-iconv": "*", "ext-iconv": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"amphp/http-client": "^5.1", "amphp/http-client": "^5.1",
"api-platform/core": "^3.1", "api-platform/core": "^3.1",
"beberlei/doctrineextensions": "^1.2", "beberlei/doctrineextensions": "^1.2",
"brick/math": "0.12.1 as 0.11.0", "brick/math": "0.12.1 as 0.11.0",
"composer/ca-bundle": "^1.5", "composer/ca-bundle": "^1.5",
"composer/package-versions-deprecated": "^1.11.99.5", "composer/package-versions-deprecated": "^1.11.99.5",
"doctrine/data-fixtures": "^2.0.0", "doctrine/data-fixtures": "^2.0.0",
"doctrine/dbal": "^4.0.0", "doctrine/dbal": "^4.0.0",
"doctrine/doctrine-bundle": "^2.0", "doctrine/doctrine-bundle": "^2.0",
"doctrine/doctrine-migrations-bundle": "^3.0", "doctrine/doctrine-migrations-bundle": "^3.0",
"doctrine/orm": "^3.2.0", "doctrine/orm": "^3.2.0",
"dompdf/dompdf": "^v3.0.0", "dompdf/dompdf": "^v3.0.0",
"erusev/parsedown": "^1.7", "erusev/parsedown": "^1.7",
"florianv/swap": "^4.0", "florianv/swap": "^4.0",
"florianv/swap-bundle": "dev-master", "florianv/swap-bundle": "dev-master",
"gregwar/captcha-bundle": "^2.1.0", "gregwar/captcha-bundle": "^2.1.0",
"hshn/base64-encoded-file": "^5.0", "hshn/base64-encoded-file": "^5.0",
"jbtronics/2fa-webauthn": "^v2.2.0", "jbtronics/2fa-webauthn": "^v2.2.0",
"jbtronics/dompdf-font-loader-bundle": "^1.0.0", "jbtronics/dompdf-font-loader-bundle": "^1.0.0",
"jfcherng/php-diff": "^6.14", "jfcherng/php-diff": "^6.14",
"knpuniversity/oauth2-client-bundle": "^2.15", "knpuniversity/oauth2-client-bundle": "^2.15",
"league/csv": "^9.8.0", "league/csv": "^9.8.0",
"league/html-to-markdown": "^5.0.1", "league/html-to-markdown": "^5.0.1",
"liip/imagine-bundle": "^2.2", "liip/imagine-bundle": "^2.2",
"nbgrp/onelogin-saml-bundle": "^1.3", "nbgrp/onelogin-saml-bundle": "^1.3",
"nelexa/zip": "^4.0", "nelexa/zip": "^4.0",
"nelmio/cors-bundle": "^2.3", "nelmio/cors-bundle": "^2.3",
"nelmio/security-bundle": "^3.0", "nelmio/security-bundle": "^3.0",
"nyholm/psr7": "^1.1", "nyholm/psr7": "^1.1",
"omines/datatables-bundle": "^0.9.1", "omines/datatables-bundle": "^0.9.1",
"paragonie/sodium_compat": "^1.21", "paragonie/sodium_compat": "^1.21",
"part-db/label-fonts": "^1.0", "part-db/label-fonts": "^1.0",
"rhukster/dom-sanitizer": "^1.0", "phpoffice/phpspreadsheet": "*",
"runtime/frankenphp-symfony": "^0.2.0", "rhukster/dom-sanitizer": "^1.0",
"s9e/text-formatter": "^2.1", "runtime/frankenphp-symfony": "^0.2.0",
"scheb/2fa-backup-code": "^6.8.0", "s9e/text-formatter": "^2.1",
"scheb/2fa-bundle": "^6.8.0", "scheb/2fa-backup-code": "^6.8.0",
"scheb/2fa-google-authenticator": "^6.8.0", "scheb/2fa-bundle": "^6.8.0",
"scheb/2fa-trusted-device": "^6.8.0", "scheb/2fa-google-authenticator": "^6.8.0",
"shivas/versioning-bundle": "^4.0", "scheb/2fa-trusted-device": "^6.8.0",
"spatie/db-dumper": "^3.3.1", "shivas/versioning-bundle": "^4.0",
"symfony/apache-pack": "^1.0", "spatie/db-dumper": "^3.3.1",
"symfony/asset": "6.4.*", "symfony/apache-pack": "^1.0",
"symfony/console": "6.4.*", "symfony/asset": "6.4.*",
"symfony/css-selector": "6.4.*", "symfony/console": "6.4.*",
"symfony/dom-crawler": "6.4.*", "symfony/css-selector": "6.4.*",
"symfony/dotenv": "6.4.*", "symfony/dom-crawler": "6.4.*",
"symfony/expression-language": "6.4.*", "symfony/dotenv": "6.4.*",
"symfony/flex": "^v2.3.1", "symfony/expression-language": "6.4.*",
"symfony/form": "6.4.*", "symfony/flex": "^v2.3.1",
"symfony/framework-bundle": "6.4.*", "symfony/form": "6.4.*",
"symfony/http-client": "6.4.*", "symfony/framework-bundle": "6.4.*",
"symfony/http-kernel": "6.4.*", "symfony/http-client": "6.4.*",
"symfony/mailer": "6.4.*", "symfony/http-kernel": "6.4.*",
"symfony/monolog-bundle": "^3.1", "symfony/mailer": "6.4.*",
"symfony/polyfill-php82": "^1.28", "symfony/monolog-bundle": "^3.1",
"symfony/process": "6.4.*", "symfony/polyfill-php82": "^1.28",
"symfony/property-access": "6.4.*", "symfony/process": "6.4.*",
"symfony/property-info": "6.4.*", "symfony/property-access": "6.4.*",
"symfony/rate-limiter": "6.4.*", "symfony/property-info": "6.4.*",
"symfony/runtime": "6.4.*", "symfony/rate-limiter": "6.4.*",
"symfony/security-bundle": "6.4.*", "symfony/runtime": "6.4.*",
"symfony/serializer": "6.4.*", "symfony/security-bundle": "6.4.*",
"symfony/string": "6.4.*", "symfony/serializer": "6.4.*",
"symfony/translation": "6.4.*", "symfony/string": "6.4.*",
"symfony/twig-bundle": "6.4.*", "symfony/translation": "6.4.*",
"symfony/ux-translator": "^2.10", "symfony/twig-bundle": "6.4.*",
"symfony/ux-turbo": "^2.0", "symfony/ux-translator": "^2.10",
"symfony/validator": "6.4.*", "symfony/ux-turbo": "^2.0",
"symfony/web-link": "6.4.*", "symfony/validator": "6.4.*",
"symfony/webpack-encore-bundle": "^v2.0.1", "symfony/web-link": "6.4.*",
"symfony/yaml": "6.4.*", "symfony/webpack-encore-bundle": "^v2.0.1",
"tecnickcom/tc-lib-barcode": "^2.1.4", "symfony/yaml": "6.4.*",
"twig/cssinliner-extra": "^3.0", "tecnickcom/tc-lib-barcode": "^2.1.4",
"twig/extra-bundle": "^3.8", "twig/cssinliner-extra": "^3.0",
"twig/html-extra": "^3.8", "twig/extra-bundle": "^3.8",
"twig/inky-extra": "^3.0", "twig/html-extra": "^3.8",
"twig/intl-extra": "^3.8", "twig/inky-extra": "^3.0",
"twig/markdown-extra": "^3.8", "twig/intl-extra": "^3.8",
"twig/string-extra": "^3.8", "twig/markdown-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0" "twig/string-extra": "^3.8",
"web-auth/webauthn-symfony-bundle": "^4.0.0"
},
"require-dev": {
"dama/doctrine-test-bundle": "^v8.0.0",
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
"ekino/phpstan-banned-code": "^v3.0.0",
"jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0.4",
"phpstan/phpstan-doctrine": "^2.0.1",
"phpstan/phpstan-strict-rules": "^2.0.1",
"phpstan/phpstan-symfony": "^2.0.0",
"phpunit/phpunit": "^9.5",
"rector/rector": "^2.0.4",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.4.*",
"symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.13",
"symfony/phpunit-bridge": "6.4.*",
"symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*",
"symplify/easy-coding-standard": "^12.0"
},
"suggest": {
"ext-bcmath": "Used to improve price calculation performance",
"ext-gmp": "Used to improve price calculation performanice"
},
"config": {
"preferred-install": {
"*": "dist"
}, },
"require-dev": { "platform": {
"dama/doctrine-test-bundle": "^v8.0.0", "php": "8.1.0"
"doctrine/doctrine-fixtures-bundle": "^4.0.0",
"ekino/phpstan-banned-code": "^v3.0.0",
"jbtronics/translation-editor-bundle": "^1.0",
"phpstan/extension-installer": "^1.0",
"phpstan/phpstan": "^2.0.4",
"phpstan/phpstan-doctrine": "^2.0.1",
"phpstan/phpstan-strict-rules": "^2.0.1",
"phpstan/phpstan-symfony": "^2.0.0",
"phpunit/phpunit": "^9.5",
"rector/rector": "^2.0.4",
"roave/security-advisories": "dev-latest",
"symfony/browser-kit": "6.4.*",
"symfony/debug-bundle": "6.4.*",
"symfony/maker-bundle": "^1.13",
"symfony/phpunit-bridge": "6.4.*",
"symfony/stopwatch": "6.4.*",
"symfony/web-profiler-bundle": "6.4.*",
"symplify/easy-coding-standard": "^12.0"
}, },
"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,
"config": { "phpstan/extension-installer": true,
"preferred-install": { "symfony/runtime": true,
"*": "dist" "php-http/discovery": true
},
"platform": {
"php": "8.1.0"
},
"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"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
],
"phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.4.*",
"docker": 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"
},
"post-install-cmd": [
"@auto-scripts"
],
"post-update-cmd": [
"@auto-scripts"
],
"phpstan": "vendor/bin/phpstan analyse src --level 5 --memory-limit 1G"
},
"conflict": {
"symfony/symfony": "*"
},
"extra": {
"symfony": {
"allow-contrib": false,
"require": "6.4.*",
"docker": true
}
}
} }

375
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "27cd0d915eab5e7cb57215a4c0b529fb", "content-hash": "74d80aa83c1a336f3a2b661e0c19c075",
"packages": [ "packages": [
{ {
"name": "amphp/amp", "name": "amphp/amp",
@ -1525,6 +1525,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",
@ -5077,6 +5156,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.9.0", "version": "2.9.0",
@ -6446,6 +6709,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.1.0", "version": "2.1.0",
@ -19005,9 +19374,9 @@
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*" "ext-mbstring": "*"
}, },
"platform-dev": [], "platform-dev": {},
"platform-overrides": { "platform-overrides": {
"php": "8.1.0" "php": "8.1.0"
}, },
"plugin-api-version": "2.3.0" "plugin-api-version": "2.6.0"
} }

View file

@ -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,

View file

@ -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);

View file

@ -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)
{ {
} }
@ -101,7 +104,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)) {
@ -188,16 +191,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)) {
@ -272,7 +279,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');
@ -328,6 +335,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);
} }
@ -347,10 +381,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.
* *

View file

@ -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'));
} }
} }

View file

@ -34,6 +34,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
@ -182,6 +185,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'];
} }
/** /**
@ -319,4 +326,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);
}
} }