Files
myep/src/Twig/AppRuntime.php
T

255 lines
8.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Twig;
use App\BusProNet\DataProvider\CountryDataProvider;
use App\Form\Model\BookingDto;
use App\Form\Service\CreateFieldStateProvider;
use App\Form\Service\EditFieldStateProvider;
use App\Service\BookingService;
use App\Service\ParticipantEligibilityService;
use Symfony\Component\Form\FormView;
use Symfony\Component\HttpFoundation\RequestStack;
use Twig\Environment;
use Twig\Extension\RuntimeExtensionInterface;
use Twig\Extra\Intl\IntlExtension;
/**
* Runtime implementation for AppExtension's filters and functions.
*
* Provides formatting utilities (money, file size, service prices), code-to-label
* mapping for display (gender, status, country), and field state introspection
* for conditional rendering in templates.
*/
class AppRuntime implements RuntimeExtensionInterface
{
public function __construct(
private readonly IntlExtension $intlExtension,
private readonly CountryDataProvider $countryDataProvider,
private readonly ParticipantEligibilityService $participantEligibilityService,
private readonly CreateFieldStateProvider $createFieldStateProvider,
private readonly EditFieldStateProvider $editFieldStateProvider,
private readonly RequestStack $requestStack,
private readonly string $environment,
) {
}
public function fileIconFilter(Environment $environment, string $mimeType, ?string $classes = 'w-4 h-4'): string
{
$icon = match ($mimeType) {
'application/pdf' => 'pdf',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'excel',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'word',
default => 'download',
};
return $this->renderIcon($environment, $icon, $classes);
}
public function formatBytes(int $bytes, ?int $precision = 2): string
{
$size = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
$factor = floor((strlen($bytes) - 1) / 3);
return sprintf("%.{$precision}f", $bytes / (1024 ** $factor)).@$size[$factor];
}
public function formatMoney(int $amount): string
{
// Amounts are stored as integers so divide by 100 first
$amount = $amount / 100;
return $this->intlExtension->formatCurrency($amount, 'EUR');
}
/**
* Formats a service price, showing "inkl." for zero-priced (included) services.
*
* @param float|int|null $price The price to format
* @param bool $showIncludedLabel Whether to show "inkl." for zero prices (default: true)
*
* @return string The formatted price, "inkl." for zero/null prices (if enabled), or empty string
*/
public function formatServicePrice(float|int|null $price, bool $showIncludedLabel = true): string
{
if (null === $price || 0 === $price || 0.0 === $price) {
return $showIncludedLabel ? 'inkl.' : '';
}
return $this->intlExtension->formatCurrency((float) $price, 'EUR');
}
public function mapStatus(string $status): string
{
$status = strtoupper($status);
return match ($status) {
'F', 'F/S' => 'Festbuchung',
'S' => 'Stornierung',
'O' => 'Option',
'U' => 'Umbuchung',
'A' => 'Anfrage',
default => '',
};
}
public function mapGender(string $gender): string
{
$gender = strtoupper($gender);
return match ($gender) {
'M' => 'männlich',
'W' => 'weiblich',
'D' => 'divers',
default => '',
};
}
public function mapCountry(?string $country): ?string
{
if (null === $country) {
return null;
}
return $this->countryDataProvider->get($country)?->name;
}
public function mapNationality(?string $nationality): ?string
{
if (null === $nationality) {
return null;
}
return $this->countryDataProvider->get($nationality)?->nationality;
}
public function isParticipantEligible(BookingDto $bookingDto, int $participantIndex): bool
{
return $this->participantEligibilityService->isParticipantEligible($bookingDto, $participantIndex);
}
/**
* Renders a QA attribute for testing purposes.
*
* @param string $label The label for the QA attribute
* @param string|null $value The value for the QA attribute
*
* @return string The rendered QA attribute
*/
public function renderQaAttribute(string $label, ?string $value = null): string
{
if ('prod' === $this->environment) {
return '';
}
if (null === $value) {
return sprintf(' data-qa-%s', strtolower($label));
}
return sprintf(' data-qa-%s="%s"', strtolower($label), $value);
}
/**
* Checks if a form field should be rendered as static text.
*
* Accepts either a FormView (when field is in form structure) or a string field name
* (when field is excluded from form but needs to be checked).
*
* @param FormView|string $field The FormView or field name
* @param BookingDto $bookingDto The booking context (create or edit mode)
* @param int $participantIndex The participant index
*
* @return bool True if field should render as static text
*/
public function isStaticText(FormView|string $field, BookingDto $bookingDto, int $participantIndex): bool
{
$fieldName = $field instanceof FormView ? $field->vars['name'] : $field;
$provider = $this->getProvider($bookingDto);
return $provider->shouldRenderAsStaticText($fieldName, $bookingDto, $participantIndex);
}
/**
* Checks if a form field should be completely hidden (not rendered at all).
*
* Accepts either a FormView (when field is in form structure) or a string field name
* (when field is excluded from form but needs to be checked).
*
* @param FormView|string $field The FormView or field name
* @param BookingDto $bookingDto The booking context (create or edit mode)
* @param int $participantIndex The participant index
*
* @return bool True if field should be hidden (not rendered)
*/
public function isHidden(FormView|string $field, BookingDto $bookingDto, int $participantIndex): bool
{
$fieldName = $field instanceof FormView ? $field->vars['name'] : $field;
$provider = $this->getProvider($bookingDto);
return !$provider->shouldIncludeField($fieldName, $bookingDto, $participantIndex);
}
/**
* Recursively collects labels of invalid form fields.
*
* @param FormView $form The form view to check
*
* @return array<string> Array of invalid field labels
*/
public function collectInvalidFieldLabels(FormView $form): array
{
$labels = [];
foreach ($form->children as $child) {
$hasErrors = \count($child->vars['errors']) > 0;
$hasInvalidChildren = false === $child->vars['valid'];
if ($hasErrors || $hasInvalidChildren) {
$hasChildren = \count($child->children) > 0;
$isExpanded = $child->vars['expanded'] ?? false;
if ($hasChildren && false === $isExpanded && false === $hasErrors) {
// Nested form without own errors (e.g., address, bodyDimensions) - recurse
$labels = array_merge($labels, $this->collectInvalidFieldLabels($child));
} else {
// Leaf field, expanded choice, or compound field with own errors
$labels[] = $child->vars['label'] ?? $child->vars['name'];
}
}
}
return $labels;
}
/**
* Returns the current booking theme from session.
*
* Defaults to 'base' if no theme is set.
*/
public function getBookingTheme(): string
{
$request = $this->requestStack->getCurrentRequest();
if (null === $request) {
return BookingService::DEFAULT_THEME;
}
return $request->getSession()->get(BookingService::THEME_KEY, BookingService::DEFAULT_THEME);
}
/**
* Gets the appropriate field state provider based on booking mode.
*
* @param BookingDto $bookingDto The booking context
*
* @return CreateFieldStateProvider|EditFieldStateProvider The appropriate provider
*/
private function getProvider(BookingDto $bookingDto): CreateFieldStateProvider|EditFieldStateProvider
{
return BookingDto::MODE_CREATE === $bookingDto->getMode()
? $this->createFieldStateProvider
: $this->editFieldStateProvider;
}
}