vcard4reseller/backend/src/Service/VCardBuilder.php
Thomas Peterson ebaf509a2f Fundament: Symfony+API-Platform-Backend & Vue-SPA (Phase 0–2)
Stack & Setup
- Dockerisierte Dev-Umgebung (PHP 8.4-FPM, Nginx, MariaDB 11.4)
- Symfony 7.4 + API Platform 4.3, Doctrine ORM, LexikJWT, Messenger
- Vue 3 + TS (Vite), Vue Router, Pinia, Axios

Kern-Domäne & Auth
- Entitäten: User, PlatformPlan, Reseller, Company, Domain, Location,
  Employee, ContactLink (UUIDv7)
- JWT-Login (/api/login), Rollen-Hierarchie, /api/me
- Mandantentrennung via API-Platform-Query-Extension (Lesen) +
  TenantStampProcessor (Schreiben)

Öffentliche Profile (SSR)
- Profil-Landingpage, vCard-Download, QR-Code im Marken-Look
- Stabiler NFC/QR-Kurz-Link /t/{code} -> Redirect aufs aktuelle Profil
- Firmenspezifisches Branding (Farben/Logo) auf der Profilseite

Verwaltungsoberfläche (SPA)
- Brand-Look (dunkle Sidebar), rollenbasierte Navigation
- Dashboard, Reseller (+Provisioning), Firmen, Mitarbeiter, Standorte,
  Domains, Design/Branding mit Live-Vorschau

Konzept & Doku: docs/KONZEPT.md (inkl. Wallet/Sync §12), README.md

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-05-31 11:12:53 +02:00

74 lines
2.2 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Service;
use App\Entity\Employee;
/**
* Erzeugt eine vCard (RFC 6350, Version 3.0 breite Kompatibilität mit
* iOS, Android, Outlook) aus einem Mitarbeiterprofil.
*/
final class VCardBuilder
{
public function build(Employee $e): string
{
$company = $e->getCompany();
$location = $e->getLocation();
$lines = [];
$lines[] = 'BEGIN:VCARD';
$lines[] = 'VERSION:3.0';
$lines[] = 'N:'.$this->esc($e->getLastName()).';'.$this->esc($e->getFirstName()).';;;';
$lines[] = 'FN:'.$this->esc(trim($e->getFirstName().' '.$e->getLastName()));
if ($company) {
$lines[] = 'ORG:'.$this->esc($company->getName());
}
if ($e->getPosition()) {
$lines[] = 'TITLE:'.$this->esc($e->getPosition());
}
if ($e->getEmail()) {
$lines[] = 'EMAIL;TYPE=WORK:'.$this->esc($e->getEmail());
}
if ($e->getPhone()) {
$lines[] = 'TEL;TYPE=WORK,VOICE:'.$this->esc($e->getPhone());
}
if ($e->getMobile()) {
$lines[] = 'TEL;TYPE=CELL:'.$this->esc($e->getMobile());
}
if ($location && ($location->getStreet() || $location->getCity())) {
// ADR: ;;Straße;Ort;;PLZ;Land
$lines[] = 'ADR;TYPE=WORK:;;'
.$this->esc((string) $location->getStreet()).';'
.$this->esc((string) $location->getCity()).';;'
.$this->esc((string) $location->getPostalCode()).';'
.$this->esc((string) $location->getCountry());
}
foreach ($e->getContactLinks() as $link) {
$lines[] = 'URL:'.$this->esc($link->getUrl());
}
if ($e->getBio()) {
$lines[] = 'NOTE:'.$this->esc($e->getBio());
}
$lines[] = 'REV:'.$e->getUpdatedAt()->format('Ymd\THis\Z');
$lines[] = 'END:VCARD';
// vCard verlangt CRLF-Zeilenenden
return implode("\r\n", $lines)."\r\n";
}
/** Escaping gemäß vCard-Spezifikation. */
private function esc(string $value): string
{
return str_replace(
['\\', "\n", ',', ';'],
['\\\\', '\\n', '\\,', '\\;'],
$value,
);
}
}