- VCardBuilder: Titel (N-Präfix), Abteilung (ORG), Privat-E-Mail, Mobil, Zentrale, Fax, Website, Geschäfts-/Privatadresse, Foto (PHOTO URI) - Profilseite: Foto über echte URL, Kontakt-Sektion (alle Nummern/Mails/Web), Adresse geschäftlich+privat, Branding via BrandingService (Reseller-Vererbung) - Mitarbeiter-Formular nutzt das breite Modal (kein Overflow mehr) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
146 lines
4.5 KiB
PHP
146 lines
4.5 KiB
PHP
<?php
|
||
|
||
namespace App\Service;
|
||
|
||
use App\Entity\Employee;
|
||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||
|
||
/**
|
||
* Erzeugt eine vCard (RFC 6350, Version 3.0 – breite Kompatibilität mit
|
||
* iOS, Android, Outlook) aus einem Mitarbeiterprofil.
|
||
*/
|
||
final class VCardBuilder
|
||
{
|
||
public function __construct(private readonly UrlGeneratorInterface $urls)
|
||
{
|
||
}
|
||
|
||
public function build(Employee $e): string
|
||
{
|
||
$company = $e->getCompany();
|
||
|
||
$lines = [];
|
||
$lines[] = 'BEGIN:VCARD';
|
||
$lines[] = 'VERSION:3.0';
|
||
// N: Nachname;Vorname;;Präfix(Titel);
|
||
$lines[] = 'N:'.$this->esc($e->getLastName()).';'.$this->esc($e->getFirstName()).';;'
|
||
.$this->esc((string) $e->getTitle()).';';
|
||
$lines[] = 'FN:'.$this->esc(trim(($e->getTitle() ? $e->getTitle().' ' : '').$e->getFirstName().' '.$e->getLastName()));
|
||
|
||
if ($company) {
|
||
// ORG:Firma;Abteilung
|
||
$org = $this->esc($company->getName());
|
||
if ($e->getDepartment()) {
|
||
$org .= ';'.$this->esc($e->getDepartment());
|
||
}
|
||
$lines[] = 'ORG:'.$org;
|
||
}
|
||
if ($e->getPosition()) {
|
||
$lines[] = 'TITLE:'.$this->esc($e->getPosition());
|
||
}
|
||
|
||
if ($e->getEmail()) {
|
||
$lines[] = 'EMAIL;TYPE=WORK:'.$this->esc($e->getEmail());
|
||
}
|
||
if ($e->getEmailPrivate()) {
|
||
$lines[] = 'EMAIL;TYPE=HOME:'.$this->esc($e->getEmailPrivate());
|
||
}
|
||
|
||
if ($e->getPhone()) {
|
||
$lines[] = 'TEL;TYPE=WORK,VOICE:'.$this->esc($e->getPhone());
|
||
}
|
||
if ($e->getMobile()) {
|
||
$lines[] = 'TEL;TYPE=CELL:'.$this->esc($e->getMobile());
|
||
}
|
||
if ($e->getPhoneCentral()) {
|
||
$lines[] = 'TEL;TYPE=WORK:'.$this->esc($e->getPhoneCentral());
|
||
}
|
||
if ($e->getFax()) {
|
||
$lines[] = 'TEL;TYPE=WORK,FAX:'.$this->esc($e->getFax());
|
||
}
|
||
|
||
if ($e->getWebsite()) {
|
||
$lines[] = 'URL:'.$this->esc($e->getWebsite());
|
||
}
|
||
|
||
$this->addAddress($lines, 'WORK', $e->getAddressBusiness() ?? $this->fromLocation($e));
|
||
$this->addAddress($lines, 'HOME', $e->getAddressPrivate());
|
||
|
||
foreach ($e->getContactLinks() as $link) {
|
||
$lines[] = 'URL:'.$this->esc($link->getUrl());
|
||
}
|
||
|
||
if (null !== $e->getPhotoPath()) {
|
||
$lines[] = 'PHOTO;VALUE=URI:'.$this->urls->generate(
|
||
'employee_photo_public',
|
||
['id' => (string) $e->getId()],
|
||
UrlGeneratorInterface::ABSOLUTE_URL,
|
||
);
|
||
}
|
||
|
||
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";
|
||
}
|
||
|
||
/**
|
||
* @param string[] $lines
|
||
* @param array<string, mixed>|null $a
|
||
*/
|
||
private function addAddress(array &$lines, string $type, ?array $a): void
|
||
{
|
||
if (null === $a) {
|
||
return;
|
||
}
|
||
$street = trim((string) ($a['street'] ?? '').' '.(string) ($a['houseNumber'] ?? ''));
|
||
$city = (string) ($a['city'] ?? '');
|
||
if ('' === $street && '' === $city) {
|
||
return;
|
||
}
|
||
// ADR: PO;Ext;Straße;Ort;Region;PLZ;Land
|
||
$lines[] = 'ADR;TYPE='.$type.':;'
|
||
.$this->esc((string) ($a['addressLine2'] ?? '')).';'
|
||
.$this->esc($street).';'
|
||
.$this->esc($city).';'
|
||
.$this->esc((string) ($a['state'] ?? '')).';'
|
||
.$this->esc((string) ($a['zip'] ?? '')).';'
|
||
.$this->esc((string) ($a['country'] ?? ''));
|
||
}
|
||
|
||
/**
|
||
* Fallback: Geschäftsadresse aus dem zugeordneten Standort.
|
||
*
|
||
* @return array<string, mixed>|null
|
||
*/
|
||
private function fromLocation(Employee $e): ?array
|
||
{
|
||
$l = $e->getLocation();
|
||
if (null === $l || (!$l->getStreet() && !$l->getCity())) {
|
||
return null;
|
||
}
|
||
|
||
return [
|
||
'street' => (string) $l->getStreet(),
|
||
'city' => (string) $l->getCity(),
|
||
'zip' => (string) $l->getPostalCode(),
|
||
'country' => (string) $l->getCountry(),
|
||
];
|
||
}
|
||
|
||
/** Escaping gemäß vCard-Spezifikation. */
|
||
private function esc(string $value): string
|
||
{
|
||
return str_replace(
|
||
['\\', "\n", ',', ';'],
|
||
['\\\\', '\\n', '\\,', '\\;'],
|
||
$value,
|
||
);
|
||
}
|
||
}
|