Skalierbarkeit: Druck-Assets in S3-Object-Storage (Flysystem)
Macht die App-Nodes zustandslos (horizontal skalierbar): Hintergrund-PDFs und Schriften liegen nicht mehr lokal, sondern im S3-kompatiblen Object Storage (Flysystem + async-aws). In der DB stehen Storage-Keys. - flysystem-bundle + async-aws (Storage "card_assets"), env-getrieben (S3_ENDPOINT/REGION/BUCKET/KEY/SECRET/PATH_STYLE) → lokal MinIO, prod Hetzner OS - CardAssetUploadController: Upload/Read/Delete über Storage; GET streamt PDF - CardPdfRenderer: liest Hintergrund (FPDI StreamReader) & Schriften (Temp-Datei) aus S3 - docker-compose: minio + minio-init (Bucket) + zweiter App-Node php2 (Profil scale-test) - app:render-card Command für den Cross-Node-Nachweis Verifiziert: Upload über Node 1 → identisches PDF-Render (51897 B, mit Hintergrund) auf Node 2, der nur DB + Object Storage liest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
73f05ed7e7
commit
67e4353c8d
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,3 +21,6 @@
|
|||||||
# Playwright-Artefakte
|
# Playwright-Artefakte
|
||||||
/.playwright-mcp/
|
/.playwright-mcp/
|
||||||
backend/.playwright-mcp/
|
backend/.playwright-mcp/
|
||||||
|
|
||||||
|
# Auto-generierte Symfony-Config-Referenz
|
||||||
|
/backend/config/reference.php
|
||||||
|
|||||||
10
backend/.env
10
backend/.env
@ -46,6 +46,16 @@ JWT_PUBLIC_KEY=%kernel.project_dir%/config/jwt/public.pem
|
|||||||
JWT_PASSPHRASE=d75959918d9ccc5c89c62edbd6e6c6af82d6e2a3d303c53a6f3328e94a05b60a
|
JWT_PASSPHRASE=d75959918d9ccc5c89c62edbd6e6c6af82d6e2a3d303c53a6f3328e94a05b60a
|
||||||
###< lexik/jwt-authentication-bundle ###
|
###< lexik/jwt-authentication-bundle ###
|
||||||
|
|
||||||
|
###> S3 / Object Storage (Druck-Assets) ###
|
||||||
|
# Lokal: MinIO. Prod: Hetzner Object Storage (Werte in .env.local / Server-Env setzen).
|
||||||
|
S3_ENDPOINT=http://minio:9000
|
||||||
|
S3_REGION=us-east-1
|
||||||
|
S3_BUCKET=card-assets
|
||||||
|
S3_KEY=minioadmin
|
||||||
|
S3_SECRET=minioadmin
|
||||||
|
S3_PATH_STYLE=true
|
||||||
|
###< S3 / Object Storage ###
|
||||||
|
|
||||||
###> symfony/messenger ###
|
###> symfony/messenger ###
|
||||||
# Choose one of the transports below
|
# Choose one of the transports below
|
||||||
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
# MESSENGER_TRANSPORT_DSN=amqp://guest:guest@localhost:5672/%2f/messages
|
||||||
|
|||||||
@ -9,10 +9,13 @@
|
|||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"api-platform/doctrine-orm": "^4.3",
|
"api-platform/doctrine-orm": "^4.3",
|
||||||
"api-platform/symfony": "^4.3",
|
"api-platform/symfony": "^4.3",
|
||||||
|
"async-aws/async-aws-bundle": "^1.17",
|
||||||
"doctrine/doctrine-bundle": "^3.2",
|
"doctrine/doctrine-bundle": "^3.2",
|
||||||
"doctrine/doctrine-migrations-bundle": "^4.0",
|
"doctrine/doctrine-migrations-bundle": "^4.0",
|
||||||
"doctrine/orm": "^3.6",
|
"doctrine/orm": "^3.6",
|
||||||
"endroid/qr-code": "^6.1",
|
"endroid/qr-code": "^6.1",
|
||||||
|
"league/flysystem-async-aws-s3": "^3.31",
|
||||||
|
"league/flysystem-bundle": "^3.7",
|
||||||
"lexik/jwt-authentication-bundle": "^3.2",
|
"lexik/jwt-authentication-bundle": "^3.2",
|
||||||
"nelmio/cors-bundle": "^2.6",
|
"nelmio/cors-bundle": "^2.6",
|
||||||
"phpdocumentor/reflection-docblock": "^6.0",
|
"phpdocumentor/reflection-docblock": "^6.0",
|
||||||
|
|||||||
783
backend/composer.lock
generated
783
backend/composer.lock
generated
@ -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": "76223bb8137f9fc4c551962833c3a836",
|
"content-hash": "56976c338e2f27b4bcb9814439daefd5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "api-platform/doctrine-common",
|
"name": "api-platform/doctrine-common",
|
||||||
@ -1161,6 +1161,217 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-05-07T11:45:31+00:00"
|
"time": "2026-05-07T11:45:31+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "async-aws/async-aws-bundle",
|
||||||
|
"version": "1.17.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/async-aws/symfony-bundle.git",
|
||||||
|
"reference": "256fb077c36f1f8f9cf55cd3e6853c4035636cfa"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/async-aws/symfony-bundle/zipball/256fb077c36f1f8f9cf55cd3e6853c4035636cfa",
|
||||||
|
"reference": "256fb077c36f1f8f9cf55cd3e6853c4035636cfa",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"async-aws/core": "^1.0",
|
||||||
|
"php": "^8.2",
|
||||||
|
"symfony/config": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/dependency-injection": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/http-kernel": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"async-aws/s3": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"async-aws/ses": "^1.0",
|
||||||
|
"async-aws/sqs": "^1.0 || ^2.0",
|
||||||
|
"async-aws/ssm": "^1.0 || ^2.0",
|
||||||
|
"matthiasnoback/symfony-config-test": "^6.1",
|
||||||
|
"nyholm/symfony-bundle-test": "^3.0",
|
||||||
|
"phpunit/phpunit": "^11.5.42",
|
||||||
|
"symfony/cache": "^4.4 || ^5.0 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/error-handler": "^7.3.2 || ^8.0",
|
||||||
|
"symfony/framework-bundle": "^5.4.45 || ^6.4.13 || ^7.1.6",
|
||||||
|
"symfony/phpunit-bridge": "^7.3.2 || ^8.0",
|
||||||
|
"symfony/runtime": "^7.3.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.17-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"AsyncAws\\Symfony\\Bundle\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Configure all your AsyncAws services and enjoy the autowire greatness.",
|
||||||
|
"keywords": [
|
||||||
|
"amazon",
|
||||||
|
"async-aws",
|
||||||
|
"aws",
|
||||||
|
"bundle",
|
||||||
|
"sdk",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/async-aws/symfony-bundle/tree/1.17.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/jderusse",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nyholm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-04-18T17:06:10+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "async-aws/core",
|
||||||
|
"version": "1.29.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/async-aws/core.git",
|
||||||
|
"reference": "70899695fcc7b23a9247926ff8b581668583b993"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/async-aws/core/zipball/70899695fcc7b23a9247926ff8b581668583b993",
|
||||||
|
"reference": "70899695fcc7b23a9247926ff8b581668583b993",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-hash": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"php": "^8.2",
|
||||||
|
"psr/cache": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"psr/log": "^1.0 || ^2.0 || ^3.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.1 || ^3.0",
|
||||||
|
"symfony/http-client": "^4.4.16 || ^5.1.7 || ^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/http-client-contracts": "^1.1.8 || ^2.0 || ^3.0",
|
||||||
|
"symfony/service-contracts": "^1.0 || ^2.0 || ^3.0"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"async-aws/s3": "<1.1",
|
||||||
|
"symfony/http-client": "5.2.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.5.42",
|
||||||
|
"symfony/error-handler": "^7.3.2 || ^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^7.3.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "1.29-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"AsyncAws\\Core\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Core package to integrate with AWS. This is a lightweight AWS SDK provider by AsyncAws.",
|
||||||
|
"keywords": [
|
||||||
|
"amazon",
|
||||||
|
"async-aws",
|
||||||
|
"aws",
|
||||||
|
"sdk",
|
||||||
|
"sts"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/async-aws/core/tree/1.29.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/jderusse",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nyholm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-04-18T17:06:10+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "async-aws/s3",
|
||||||
|
"version": "3.3.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/async-aws/s3.git",
|
||||||
|
"reference": "26bf7e498df4fd135fb2c32cbaa5ba92c0615fa3"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/async-aws/s3/zipball/26bf7e498df4fd135fb2c32cbaa5ba92c0615fa3",
|
||||||
|
"reference": "26bf7e498df4fd135fb2c32cbaa5ba92c0615fa3",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"async-aws/core": "^1.22",
|
||||||
|
"ext-dom": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
|
"ext-hash": "*",
|
||||||
|
"ext-simplexml": "*",
|
||||||
|
"php": "^8.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"phpunit/phpunit": "^11.5.42",
|
||||||
|
"symfony/error-handler": "^7.3.2 || ^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^7.3.2 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-master": "3.3-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"AsyncAws\\S3\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "S3 client, part of the AWS SDK provided by AsyncAws.",
|
||||||
|
"keywords": [
|
||||||
|
"amazon",
|
||||||
|
"async-aws",
|
||||||
|
"aws",
|
||||||
|
"s3",
|
||||||
|
"sdk"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/async-aws/s3/tree/3.3.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/jderusse",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nyholm",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-04-29T10:05:33+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "bacon/bacon-qr-code",
|
"name": "bacon/bacon-qr-code",
|
||||||
"version": "v3.1.1",
|
"version": "v3.1.1",
|
||||||
@ -2693,6 +2904,322 @@
|
|||||||
],
|
],
|
||||||
"time": "2025-10-17T11:30:53+00:00"
|
"time": "2025-10-17T11:30:53+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "league/flysystem",
|
||||||
|
"version": "3.34.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/flysystem.git",
|
||||||
|
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
|
||||||
|
"reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/flysystem-local": "^3.0.0",
|
||||||
|
"league/mime-type-detection": "^1.0.0",
|
||||||
|
"php": "^8.0.2"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"async-aws/core": "<1.19.0",
|
||||||
|
"async-aws/s3": "<1.14.0",
|
||||||
|
"aws/aws-sdk-php": "3.209.31 || 3.210.0",
|
||||||
|
"guzzlehttp/guzzle": "<7.0",
|
||||||
|
"guzzlehttp/ringphp": "<1.1.1",
|
||||||
|
"phpseclib/phpseclib": "3.0.15",
|
||||||
|
"symfony/http-client": "<5.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"async-aws/s3": "^1.5 || ^2.0",
|
||||||
|
"async-aws/simple-s3": "^1.1 || ^2.0",
|
||||||
|
"aws/aws-sdk-php": "^3.295.10",
|
||||||
|
"composer/semver": "^3.0",
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"ext-ftp": "*",
|
||||||
|
"ext-mongodb": "^1.3|^2",
|
||||||
|
"ext-zip": "*",
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.5",
|
||||||
|
"google/cloud-storage": "^1.23",
|
||||||
|
"guzzlehttp/psr7": "^2.6",
|
||||||
|
"microsoft/azure-storage-blob": "^1.1",
|
||||||
|
"mongodb/mongodb": "^1.2|^2",
|
||||||
|
"phpseclib/phpseclib": "^3.0.36",
|
||||||
|
"phpstan/phpstan": "^1.10",
|
||||||
|
"phpunit/phpunit": "^9.5.11|^10.0",
|
||||||
|
"sabre/dav": "^4.6.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Flysystem\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Frank de Jonge",
|
||||||
|
"email": "info@frankdejonge.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "File storage abstraction for PHP",
|
||||||
|
"keywords": [
|
||||||
|
"WebDAV",
|
||||||
|
"aws",
|
||||||
|
"cloud",
|
||||||
|
"file",
|
||||||
|
"files",
|
||||||
|
"filesystem",
|
||||||
|
"filesystems",
|
||||||
|
"ftp",
|
||||||
|
"s3",
|
||||||
|
"sftp",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thephpleague/flysystem/issues",
|
||||||
|
"source": "https://github.com/thephpleague/flysystem/tree/3.34.0"
|
||||||
|
},
|
||||||
|
"time": "2026-05-14T10:28:08+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "league/flysystem-async-aws-s3",
|
||||||
|
"version": "3.31.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/flysystem-async-aws-s3.git",
|
||||||
|
"reference": "6928568d3fe24afc4d7d583fc1844d0c1d07651b"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/flysystem-async-aws-s3/zipball/6928568d3fe24afc4d7d583fc1844d0c1d07651b",
|
||||||
|
"reference": "6928568d3fe24afc4d7d583fc1844d0c1d07651b",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"async-aws/s3": "^1.5 || ^2.0 || ^3.0",
|
||||||
|
"league/flysystem": "^3.10.0",
|
||||||
|
"league/mime-type-detection": "^1.0.0",
|
||||||
|
"php": "^8.0.2"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"symfony/http-client": "<5.2"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"async-aws/simple-s3": "^2.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Flysystem\\AsyncAwsS3\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Frank de Jonge",
|
||||||
|
"email": "info@frankdejonge.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "AsyncAws S3 filesystem adapter for Flysystem.",
|
||||||
|
"keywords": [
|
||||||
|
"Flysystem",
|
||||||
|
"async-aws",
|
||||||
|
"aws",
|
||||||
|
"file",
|
||||||
|
"files",
|
||||||
|
"filesystem",
|
||||||
|
"s3",
|
||||||
|
"storage"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/thephpleague/flysystem-async-aws-s3/tree/3.31.0"
|
||||||
|
},
|
||||||
|
"time": "2026-01-23T15:30:45+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "league/flysystem-bundle",
|
||||||
|
"version": "3.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/flysystem-bundle.git",
|
||||||
|
"reference": "5eb41be38fc3759f74c9e458a6a5f0ef5f49284a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/5eb41be38fc3759f74c9e458a6a5f0ef5f49284a",
|
||||||
|
"reference": "5eb41be38fc3759f74c9e458a6a5f0ef5f49284a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"league/flysystem": "^3.0",
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/config": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/deprecation-contracts": "^2.1 || ^3",
|
||||||
|
"symfony/http-kernel": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/options-resolver": "^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/mongodb-odm": "^2.0",
|
||||||
|
"league/flysystem-async-aws-s3": "^3.1",
|
||||||
|
"league/flysystem-aws-s3-v3": "^3.1",
|
||||||
|
"league/flysystem-azure-blob-storage": "^3.1",
|
||||||
|
"league/flysystem-ftp": "^3.1",
|
||||||
|
"league/flysystem-google-cloud-storage": "^3.1",
|
||||||
|
"league/flysystem-gridfs": "^3.28",
|
||||||
|
"league/flysystem-memory": "^3.1",
|
||||||
|
"league/flysystem-read-only": "^3.15",
|
||||||
|
"league/flysystem-sftp-v3": "^3.1",
|
||||||
|
"league/flysystem-webdav": "^3.29",
|
||||||
|
"platformcommunity/flysystem-bunnycdn": "^3.3",
|
||||||
|
"symfony/dotenv": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/framework-bundle": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/phpunit-bridge": "^6.0 || ^7.0 || ^8.0",
|
||||||
|
"symfony/var-dumper": "^6.0 || ^7.0 || ^8.0"
|
||||||
|
},
|
||||||
|
"type": "symfony-bundle",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\FlysystemBundle\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Titouan Galopin",
|
||||||
|
"email": "galopintitouan@gmail.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Symfony bundle integrating Flysystem into Symfony applications",
|
||||||
|
"keywords": [
|
||||||
|
"Flysystem",
|
||||||
|
"bundle",
|
||||||
|
"filesystem",
|
||||||
|
"symfony"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thephpleague/flysystem-bundle/issues",
|
||||||
|
"source": "https://github.com/thephpleague/flysystem-bundle/tree/3.7.0"
|
||||||
|
},
|
||||||
|
"time": "2026-03-28T22:14:56+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "league/flysystem-local",
|
||||||
|
"version": "3.31.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/flysystem-local.git",
|
||||||
|
"reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079",
|
||||||
|
"reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"league/flysystem": "^3.0.0",
|
||||||
|
"league/mime-type-detection": "^1.0.0",
|
||||||
|
"php": "^8.0.2"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\Flysystem\\Local\\": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Frank de Jonge",
|
||||||
|
"email": "info@frankdejonge.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Local filesystem adapter for Flysystem.",
|
||||||
|
"keywords": [
|
||||||
|
"Flysystem",
|
||||||
|
"file",
|
||||||
|
"files",
|
||||||
|
"filesystem",
|
||||||
|
"local"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0"
|
||||||
|
},
|
||||||
|
"time": "2026-01-23T15:30:45+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "league/mime-type-detection",
|
||||||
|
"version": "1.16.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/thephpleague/mime-type-detection.git",
|
||||||
|
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||||
|
"reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-fileinfo": "*",
|
||||||
|
"php": "^7.4 || ^8.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"friendsofphp/php-cs-fixer": "^3.2",
|
||||||
|
"phpstan/phpstan": "^0.12.68",
|
||||||
|
"phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"League\\MimeTypeDetection\\": "src"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Frank de Jonge",
|
||||||
|
"email": "info@frankdejonge.nl"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Mime-type detection for Flysystem",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
|
||||||
|
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/frankdejonge",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2024-09-21T08:32:55+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "lexik/jwt-authentication-bundle",
|
"name": "lexik/jwt-authentication-bundle",
|
||||||
"version": "v3.2.0",
|
"version": "v3.2.0",
|
||||||
@ -5017,6 +5544,189 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-05-23T18:04:28+00:00"
|
"time": "2026-05-23T18:04:28+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client",
|
||||||
|
"version": "v7.4.13",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client.git",
|
||||||
|
"reference": "e8a112b8415707265a7e614278136a9d92989a6a"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client/zipball/e8a112b8415707265a7e614278136a9d92989a6a",
|
||||||
|
"reference": "e8a112b8415707265a7e614278136a9d92989a6a",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"psr/log": "^1|^2|^3",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3",
|
||||||
|
"symfony/http-client-contracts": "~3.4.4|^3.5.2",
|
||||||
|
"symfony/polyfill-php83": "^1.29",
|
||||||
|
"symfony/service-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"amphp/amp": "<2.5",
|
||||||
|
"amphp/socket": "<1.1",
|
||||||
|
"php-http/discovery": "<1.15",
|
||||||
|
"symfony/http-foundation": "<6.4"
|
||||||
|
},
|
||||||
|
"provide": {
|
||||||
|
"php-http/async-client-implementation": "*",
|
||||||
|
"php-http/client-implementation": "*",
|
||||||
|
"psr/http-client-implementation": "1.0",
|
||||||
|
"symfony/http-client-implementation": "3.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"amphp/http-client": "^4.2.1|^5.0",
|
||||||
|
"amphp/http-tunnel": "^1.0|^2.0",
|
||||||
|
"guzzlehttp/promises": "^1.4|^2.0",
|
||||||
|
"nyholm/psr7": "^1.0",
|
||||||
|
"php-http/httplug": "^1.0|^2.0",
|
||||||
|
"psr/http-client": "^1.0",
|
||||||
|
"symfony/amphp-http-client-meta": "^1.0|^2.0",
|
||||||
|
"symfony/cache": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/dependency-injection": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/http-kernel": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/messenger": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/process": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/rate-limiter": "^6.4|^7.0|^8.0",
|
||||||
|
"symfony/stopwatch": "^6.4|^7.0|^8.0"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides powerful methods to fetch HTTP resources synchronously or asynchronously",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"http"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client/tree/v7.4.13"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-24T09:57:54+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/http-client-contracts",
|
||||||
|
"version": "v3.7.0",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/http-client-contracts.git",
|
||||||
|
"reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/http-client-contracts/zipball/4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d",
|
||||||
|
"reference": "4a2d00c37651c0bdc2b9e1c773487a8bf4edb12d",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"thanks": {
|
||||||
|
"url": "https://github.com/symfony/contracts",
|
||||||
|
"name": "symfony/contracts"
|
||||||
|
},
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-main": "3.7-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Contracts\\HttpClient\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Test/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Nicolas Grekas",
|
||||||
|
"email": "p@tchwork.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Generic abstractions related to HTTP clients",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"abstractions",
|
||||||
|
"contracts",
|
||||||
|
"decoupling",
|
||||||
|
"interfaces",
|
||||||
|
"interoperability",
|
||||||
|
"standards"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/http-client-contracts/tree/v3.7.0"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-06T13:17:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/http-foundation",
|
"name": "symfony/http-foundation",
|
||||||
"version": "v7.4.13",
|
"version": "v7.4.13",
|
||||||
@ -5312,6 +6022,77 @@
|
|||||||
],
|
],
|
||||||
"time": "2026-05-19T07:02:47+00:00"
|
"time": "2026-05-19T07:02:47+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "symfony/options-resolver",
|
||||||
|
"version": "v7.4.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/symfony/options-resolver.git",
|
||||||
|
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
|
||||||
|
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": ">=8.2",
|
||||||
|
"symfony/deprecation-contracts": "^2.5|^3"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Symfony\\Component\\OptionsResolver\\": ""
|
||||||
|
},
|
||||||
|
"exclude-from-classmap": [
|
||||||
|
"/Tests/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Fabien Potencier",
|
||||||
|
"email": "fabien@symfony.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Symfony Community",
|
||||||
|
"homepage": "https://symfony.com/contributors"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Provides an improved replacement for the array_replace PHP function",
|
||||||
|
"homepage": "https://symfony.com",
|
||||||
|
"keywords": [
|
||||||
|
"config",
|
||||||
|
"configuration",
|
||||||
|
"options"
|
||||||
|
],
|
||||||
|
"support": {
|
||||||
|
"source": "https://github.com/symfony/options-resolver/tree/v7.4.8"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://symfony.com/sponsor",
|
||||||
|
"type": "custom"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/fabpot",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://github.com/nicolas-grekas",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
|
||||||
|
"type": "tidelift"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-03-24T13:12:05+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "symfony/password-hasher",
|
"name": "symfony/password-hasher",
|
||||||
"version": "v7.4.8",
|
"version": "v7.4.8",
|
||||||
|
|||||||
@ -10,4 +10,6 @@ return [
|
|||||||
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
ApiPlatform\Symfony\Bundle\ApiPlatformBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
Lexik\Bundle\JWTAuthenticationBundle\LexikJWTAuthenticationBundle::class => ['all' => true],
|
||||||
|
League\FlysystemBundle\FlysystemBundle::class => ['all' => true],
|
||||||
|
AsyncAws\Symfony\Bundle\AsyncAwsBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
11
backend/config/packages/async_aws.yaml
Normal file
11
backend/config/packages/async_aws.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
async_aws:
|
||||||
|
clients:
|
||||||
|
card_assets:
|
||||||
|
type: s3
|
||||||
|
config:
|
||||||
|
region: '%env(S3_REGION)%'
|
||||||
|
endpoint: '%env(S3_ENDPOINT)%'
|
||||||
|
# Path-Style für MinIO (true). Hetzner Object Storage funktioniert auch mit true.
|
||||||
|
pathStyleEndpoint: '%env(bool:S3_PATH_STYLE)%'
|
||||||
|
accessKeyId: '%env(S3_KEY)%'
|
||||||
|
accessKeySecret: '%env(S3_SECRET)%'
|
||||||
11
backend/config/packages/flysystem.yaml
Normal file
11
backend/config/packages/flysystem.yaml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
# https://github.com/thephpleague/flysystem-bundle
|
||||||
|
flysystem:
|
||||||
|
storages:
|
||||||
|
# Druck-Assets (Hintergrund-PDFs, Schriften) im S3-kompatiblen Object Storage.
|
||||||
|
# Lokal: MinIO. Prod: Hetzner Object Storage. Gleicher Code, andere Env.
|
||||||
|
card_assets.storage:
|
||||||
|
adapter: 'asyncaws'
|
||||||
|
options:
|
||||||
|
client: 'async_aws.client.card_assets'
|
||||||
|
bucket: '%env(S3_BUCKET)%'
|
||||||
|
prefix: 'cards'
|
||||||
62
backend/src/Command/RenderCardCommand.php
Normal file
62
backend/src/Command/RenderCardCommand.php
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Command;
|
||||||
|
|
||||||
|
use App\Repository\CardTemplateRepository;
|
||||||
|
use App\Repository\EmployeeRepository;
|
||||||
|
use App\Service\CardPdfRenderer;
|
||||||
|
use App\Service\CardTemplateFactory;
|
||||||
|
use Symfony\Component\Console\Attribute\AsCommand;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\InputArgument;
|
||||||
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test-Utility: rendert die Visitenkarte eines Mitarbeiters und schreibt sie
|
||||||
|
* nach /tmp. Dient dem Skalierungs-Nachweis (auf mehreren Nodes ausführen –
|
||||||
|
* jeder Node liest dieselbe DB + denselben Object Storage).
|
||||||
|
*/
|
||||||
|
#[AsCommand(name: 'app:render-card', description: 'Rendert eine Visitenkarte (Skalierungs-Test).')]
|
||||||
|
final class RenderCardCommand extends Command
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly EmployeeRepository $employees,
|
||||||
|
private readonly CardTemplateRepository $templates,
|
||||||
|
private readonly CardTemplateFactory $factory,
|
||||||
|
private readonly CardPdfRenderer $renderer,
|
||||||
|
) {
|
||||||
|
parent::__construct();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function configure(): void
|
||||||
|
{
|
||||||
|
$this->addArgument('slug', InputArgument::REQUIRED, 'Employee-Slug');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
|
{
|
||||||
|
$employee = $this->employees->findOneBy(['slug' => $input->getArgument('slug')]);
|
||||||
|
if (null === $employee) {
|
||||||
|
$output->writeln('<error>Mitarbeiter nicht gefunden.</error>');
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$template = $this->templates->findCardForCompany($employee->getCompany()) ?? $this->factory->default();
|
||||||
|
$pdf = $this->renderer->render($employee, $template);
|
||||||
|
|
||||||
|
$file = sprintf('/tmp/render-%s.pdf', gethostname());
|
||||||
|
file_put_contents($file, $pdf);
|
||||||
|
|
||||||
|
$output->writeln(sprintf(
|
||||||
|
'Node %s: %d bytes, Hintergrund=%s, Datei=%s',
|
||||||
|
gethostname(),
|
||||||
|
strlen($pdf),
|
||||||
|
$template->getBackgroundPath() ? 'ja' : 'nein',
|
||||||
|
$file,
|
||||||
|
));
|
||||||
|
|
||||||
|
return Command::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,12 +7,13 @@ use App\Entity\Company;
|
|||||||
use App\Repository\CardTemplateRepository;
|
use App\Repository\CardTemplateRepository;
|
||||||
use App\Security\TenantContext;
|
use App\Security\TenantContext;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use League\Flysystem\FilesystemOperator;
|
||||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||||
use Symfony\Component\HttpFoundation\Request;
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||||
@ -21,8 +22,9 @@ use Symfony\Component\Security\Http\Attribute\IsGranted;
|
|||||||
use Symfony\Component\Uid\Uuid;
|
use Symfony\Component\Uid\Uuid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt Druck-Assets einer Firma hoch: Hintergrund-PDF (Variable Data Printing)
|
* Lädt Druck-Assets einer Firma in den (S3-)Object-Storage: Hintergrund-PDF
|
||||||
* und eigene Schriften (eingebettet ins PDF). Dateien liegen außerhalb des Webroots.
|
* (Variable Data Printing) und eigene Schriften. In der DB stehen Storage-Keys,
|
||||||
|
* keine lokalen Pfade — dadurch nodeübergreifend lesbar (horizontal skalierbar).
|
||||||
*/
|
*/
|
||||||
#[IsGranted('ROLE_COMPANY_ADMIN')]
|
#[IsGranted('ROLE_COMPANY_ADMIN')]
|
||||||
final class CardAssetUploadController
|
final class CardAssetUploadController
|
||||||
@ -31,8 +33,8 @@ final class CardAssetUploadController
|
|||||||
private readonly CardTemplateRepository $templates,
|
private readonly CardTemplateRepository $templates,
|
||||||
private readonly EntityManagerInterface $em,
|
private readonly EntityManagerInterface $em,
|
||||||
private readonly TenantContext $tenant,
|
private readonly TenantContext $tenant,
|
||||||
#[Autowire('%kernel.project_dir%/var/storage/cards')]
|
#[Autowire(service: 'card_assets.storage')]
|
||||||
private readonly string $storageDir,
|
private readonly FilesystemOperator $cardAssets,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,30 +43,31 @@ final class CardAssetUploadController
|
|||||||
{
|
{
|
||||||
$company = $this->company($id);
|
$company = $this->company($id);
|
||||||
$file = $this->file($request);
|
$file = $this->file($request);
|
||||||
if (!in_array(strtolower((string) $file->getClientOriginalExtension()), ['pdf'], true)) {
|
if ('pdf' !== strtolower((string) $file->getClientOriginalExtension())) {
|
||||||
throw new BadRequestHttpException('Nur PDF erlaubt.');
|
throw new BadRequestHttpException('Nur PDF erlaubt.');
|
||||||
}
|
}
|
||||||
|
|
||||||
$template = $this->getOrCreate($company);
|
$template = $this->getOrCreate($company);
|
||||||
$path = $this->store($file, $company->getId(), 'background', 'pdf');
|
$key = $this->store($file, $company->getId(), 'background', 'pdf');
|
||||||
$template->setBackgroundPath($path);
|
$template->setBackgroundPath($key);
|
||||||
$this->em->persist($template);
|
$this->em->persist($template);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(['backgroundPath' => $path, 'fileName' => $file->getClientOriginalName()], 201);
|
return new JsonResponse(['backgroundKey' => $key, 'fileName' => $file->getClientOriginalName()], 201);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/companies/{id}/card-template/background', name: 'card_bg_get', methods: ['GET'])]
|
#[Route('/api/companies/{id}/card-template/background', name: 'card_bg_get', methods: ['GET'])]
|
||||||
public function getBackground(string $id): Response
|
public function getBackground(string $id): Response
|
||||||
{
|
{
|
||||||
$company = $this->company($id);
|
$company = $this->company($id);
|
||||||
$template = $this->templates->findCardForCompany($company);
|
$key = $this->templates->findCardForCompany($company)?->getBackgroundPath();
|
||||||
$path = $template?->getBackgroundPath();
|
if (!$key || !$this->cardAssets->fileExists($key)) {
|
||||||
if (!$path || !is_file($path)) {
|
|
||||||
throw new NotFoundHttpException('Kein Hintergrund-PDF.');
|
throw new NotFoundHttpException('Kein Hintergrund-PDF.');
|
||||||
}
|
}
|
||||||
|
|
||||||
return new BinaryFileResponse($path, 200, ['Content-Type' => 'application/pdf']);
|
return new StreamedResponse(function () use ($key) {
|
||||||
|
fpassthru($this->cardAssets->readStream($key));
|
||||||
|
}, 200, ['Content-Type' => 'application/pdf']);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/companies/{id}/card-template/background', name: 'card_bg_delete', methods: ['DELETE'])]
|
#[Route('/api/companies/{id}/card-template/background', name: 'card_bg_delete', methods: ['DELETE'])]
|
||||||
@ -72,15 +75,15 @@ final class CardAssetUploadController
|
|||||||
{
|
{
|
||||||
$company = $this->company($id);
|
$company = $this->company($id);
|
||||||
$template = $this->templates->findCardForCompany($company);
|
$template = $this->templates->findCardForCompany($company);
|
||||||
if ($template) {
|
if ($template && $template->getBackgroundPath()) {
|
||||||
if ($template->getBackgroundPath() && is_file($template->getBackgroundPath())) {
|
if ($this->cardAssets->fileExists($template->getBackgroundPath())) {
|
||||||
@unlink($template->getBackgroundPath());
|
$this->cardAssets->delete($template->getBackgroundPath());
|
||||||
}
|
}
|
||||||
$template->setBackgroundPath(null);
|
$template->setBackgroundPath(null);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
}
|
}
|
||||||
|
|
||||||
return new JsonResponse(['backgroundPath' => null]);
|
return new JsonResponse(['backgroundKey' => null]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/api/companies/{id}/card-template/font', name: 'card_font_upload', methods: ['POST'])]
|
#[Route('/api/companies/{id}/card-template/font', name: 'card_font_upload', methods: ['POST'])]
|
||||||
@ -95,8 +98,8 @@ final class CardAssetUploadController
|
|||||||
$family = trim((string) $request->request->get('family')) ?: pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
$family = trim((string) $request->request->get('family')) ?: pathinfo($file->getClientOriginalName(), PATHINFO_FILENAME);
|
||||||
|
|
||||||
$template = $this->getOrCreate($company);
|
$template = $this->getOrCreate($company);
|
||||||
$path = $this->store($file, $company->getId(), 'font', $ext);
|
$key = $this->store($file, $company->getId(), 'font', $ext);
|
||||||
$template->addFont($family, $path);
|
$template->addFont($family, $key);
|
||||||
$this->em->flush();
|
$this->em->flush();
|
||||||
|
|
||||||
return new JsonResponse(['fonts' => $template->getFonts()], 201);
|
return new JsonResponse(['fonts' => $template->getFonts()], 201);
|
||||||
@ -112,16 +115,13 @@ final class CardAssetUploadController
|
|||||||
return $file;
|
return $file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Lädt die Datei in den Object-Storage und liefert den Key zurück. */
|
||||||
private function store(UploadedFile $file, Uuid $companyId, string $prefix, string $ext): string
|
private function store(UploadedFile $file, Uuid $companyId, string $prefix, string $ext): string
|
||||||
{
|
{
|
||||||
$dir = $this->storageDir.'/'.$companyId->toRfc4122();
|
$key = sprintf('%s/%s-%s.%s', $companyId->toRfc4122(), $prefix, bin2hex(random_bytes(4)), $ext);
|
||||||
if (!is_dir($dir)) {
|
$this->cardAssets->write($key, (string) file_get_contents($file->getPathname()));
|
||||||
@mkdir($dir, 0775, true);
|
|
||||||
}
|
|
||||||
$name = $prefix.'-'.bin2hex(random_bytes(4)).'.'.$ext;
|
|
||||||
$file->move($dir, $name);
|
|
||||||
|
|
||||||
return $dir.'/'.$name;
|
return $key;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function getOrCreate(Company $company): CardTemplate
|
private function getOrCreate(Company $company): CardTemplate
|
||||||
|
|||||||
@ -7,6 +7,9 @@ use App\Entity\Employee;
|
|||||||
use Endroid\QrCode\Builder\Builder;
|
use Endroid\QrCode\Builder\Builder;
|
||||||
use Endroid\QrCode\ErrorCorrectionLevel;
|
use Endroid\QrCode\ErrorCorrectionLevel;
|
||||||
use Endroid\QrCode\Writer\PngWriter;
|
use Endroid\QrCode\Writer\PngWriter;
|
||||||
|
use League\Flysystem\FilesystemOperator;
|
||||||
|
use setasign\Fpdi\PdfParser\StreamReader;
|
||||||
|
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -21,8 +24,11 @@ final class CardPdfRenderer
|
|||||||
/** @var array<string, string> family → eingebetteter TCPDF-Fontname */
|
/** @var array<string, string> family → eingebetteter TCPDF-Fontname */
|
||||||
private array $fontMap = [];
|
private array $fontMap = [];
|
||||||
|
|
||||||
public function __construct(private readonly UrlGeneratorInterface $urls)
|
public function __construct(
|
||||||
{
|
private readonly UrlGeneratorInterface $urls,
|
||||||
|
#[Autowire(service: 'card_assets.storage')]
|
||||||
|
private readonly FilesystemOperator $cardAssets,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
public function render(Employee $employee, CardTemplate $template): string
|
public function render(Employee $employee, CardTemplate $template): string
|
||||||
@ -30,8 +36,9 @@ final class CardPdfRenderer
|
|||||||
$branding = $this->branding($employee);
|
$branding = $this->branding($employee);
|
||||||
$bleed = $template->getBleedMm();
|
$bleed = $template->getBleedMm();
|
||||||
|
|
||||||
$bgPath = $template->getBackgroundPath();
|
$bgKey = $template->getBackgroundPath();
|
||||||
$hasBg = $bgPath && is_file($bgPath);
|
$hasBg = $bgKey && $this->cardAssets->fileExists($bgKey);
|
||||||
|
$bgReader = $hasBg ? StreamReader::createByString($this->cardAssets->read($bgKey)) : null;
|
||||||
|
|
||||||
// Mit Hintergrund-PDF: Seite = Endformat+Beschnitt, keine eigenen Schnittmarken
|
// Mit Hintergrund-PDF: Seite = Endformat+Beschnitt, keine eigenen Schnittmarken
|
||||||
// (der Kunde liefert Beschnitt/Marken selbst). Sonst Rand für Schnittmarken.
|
// (der Kunde liefert Beschnitt/Marken selbst). Sonst Rand für Schnittmarken.
|
||||||
@ -51,7 +58,7 @@ final class CardPdfRenderer
|
|||||||
|
|
||||||
$this->fontMap = $this->registerFonts($template);
|
$this->fontMap = $this->registerFonts($template);
|
||||||
|
|
||||||
$bgPages = $hasBg ? $pdf->setSourceFile($bgPath) : 0;
|
$bgPages = $hasBg ? $pdf->setSourceFile($bgReader) : 0;
|
||||||
|
|
||||||
foreach ([$template->getFront(), $template->getBack()] as $i => $elements) {
|
foreach ([$template->getFront(), $template->getBack()] as $i => $elements) {
|
||||||
$pdf->AddPage('L', [$pw, $ph]);
|
$pdf->AddPage('L', [$pw, $ph]);
|
||||||
@ -79,14 +86,20 @@ final class CardPdfRenderer
|
|||||||
{
|
{
|
||||||
$map = [];
|
$map = [];
|
||||||
foreach ($template->getFonts() as $f) {
|
foreach ($template->getFonts() as $f) {
|
||||||
$path = $f['path'] ?? '';
|
$key = $f['path'] ?? '';
|
||||||
$family = $f['family'] ?? '';
|
$family = $f['family'] ?? '';
|
||||||
if ('' !== $family && is_file($path)) {
|
if ('' === $family || '' === $key || !$this->cardAssets->fileExists($key)) {
|
||||||
try {
|
continue;
|
||||||
$map[$family] = \TCPDF_FONTS::addTTFfont($path, 'TrueTypeUnicode', '', 32);
|
}
|
||||||
} catch (\Throwable) {
|
// TCPDF braucht eine echte Datei → Schrift aus dem Storage in eine Temp-Datei
|
||||||
// nicht konvertierbar → Fallback auf Core-Font
|
$tmp = tempnam(sys_get_temp_dir(), 'fnt');
|
||||||
}
|
file_put_contents($tmp, $this->cardAssets->read($key));
|
||||||
|
try {
|
||||||
|
$map[$family] = \TCPDF_FONTS::addTTFfont($tmp, 'TrueTypeUnicode', '', 32);
|
||||||
|
} catch (\Throwable) {
|
||||||
|
// nicht konvertierbar → Fallback auf Core-Font
|
||||||
|
} finally {
|
||||||
|
@unlink($tmp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,6 +13,9 @@
|
|||||||
"src/ApiResource/.gitignore"
|
"src/ApiResource/.gitignore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"async-aws/async-aws-bundle": {
|
||||||
|
"version": "1.17.0"
|
||||||
|
},
|
||||||
"doctrine/deprecations": {
|
"doctrine/deprecations": {
|
||||||
"version": "1.1",
|
"version": "1.1",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
@ -49,6 +52,19 @@
|
|||||||
"migrations/.gitignore"
|
"migrations/.gitignore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"league/flysystem-bundle": {
|
||||||
|
"version": "3.7",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "1.0",
|
||||||
|
"ref": "913dc3d7a5a1af0d2b044c5ac3a16e2f851d7380"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"config/packages/flysystem.yaml",
|
||||||
|
"var/storage/.gitignore"
|
||||||
|
]
|
||||||
|
},
|
||||||
"lexik/jwt-authentication-bundle": {
|
"lexik/jwt-authentication-bundle": {
|
||||||
"version": "3.2",
|
"version": "3.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
@ -13,6 +13,22 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
DATABASE_URL: "mysql://app:app@mariadb:3306/vcard4reseller?serverVersion=11.4.0-MariaDB&charset=utf8mb4"
|
DATABASE_URL: "mysql://app:app@mariadb:3306/vcard4reseller?serverVersion=11.4.0-MariaDB&charset=utf8mb4"
|
||||||
|
|
||||||
|
# Zweiter, identischer App-Node — zum Beweis, dass Assets/Auth nodeübergreifend laufen
|
||||||
|
php2:
|
||||||
|
build:
|
||||||
|
context: ./docker/php
|
||||||
|
args:
|
||||||
|
UID: ${UID:-1000}
|
||||||
|
GID: ${GID:-1000}
|
||||||
|
volumes:
|
||||||
|
- ./backend:/app
|
||||||
|
depends_on:
|
||||||
|
mariadb:
|
||||||
|
condition: service_healthy
|
||||||
|
environment:
|
||||||
|
DATABASE_URL: "mysql://app:app@mariadb:3306/vcard4reseller?serverVersion=11.4.0-MariaDB&charset=utf8mb4"
|
||||||
|
profiles: ["scale-test"]
|
||||||
|
|
||||||
nginx:
|
nginx:
|
||||||
image: nginx:1.27-alpine
|
image: nginx:1.27-alpine
|
||||||
ports:
|
ports:
|
||||||
@ -23,6 +39,29 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
- php
|
- php
|
||||||
|
|
||||||
|
minio:
|
||||||
|
image: minio/minio:latest
|
||||||
|
command: server /data --console-address ":9001"
|
||||||
|
environment:
|
||||||
|
MINIO_ROOT_USER: minioadmin
|
||||||
|
MINIO_ROOT_PASSWORD: minioadmin
|
||||||
|
ports:
|
||||||
|
- "9000:9000"
|
||||||
|
- "9001:9001"
|
||||||
|
volumes:
|
||||||
|
- minio_data:/data
|
||||||
|
|
||||||
|
# Legt den Bucket einmalig an
|
||||||
|
minio-init:
|
||||||
|
image: minio/mc
|
||||||
|
depends_on:
|
||||||
|
- minio
|
||||||
|
entrypoint: >
|
||||||
|
/bin/sh -c "
|
||||||
|
until mc alias set local http://minio:9000 minioadmin minioadmin; do sleep 1; done;
|
||||||
|
mc mb -p local/card-assets || true;
|
||||||
|
"
|
||||||
|
|
||||||
mariadb:
|
mariadb:
|
||||||
image: mariadb:11.4
|
image: mariadb:11.4
|
||||||
ports:
|
ports:
|
||||||
@ -52,3 +91,4 @@ services:
|
|||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
mariadb_data:
|
mariadb_data:
|
||||||
|
minio_data:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user