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>
63 lines
2.1 KiB
PHP
63 lines
2.1 KiB
PHP
<?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;
|
||
}
|
||
}
|