Teren-app/app/Services/Documents/TokenValueResolver.php
Simon Pocrnjič e0303ece74 documents
2025-10-12 12:24:17 +02:00

208 lines
8.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\Services\Documents;
use App\Models\Contract;
use App\Models\DocumentTemplate;
use App\Models\User;
class TokenValueResolver
{
/**
* Resolve tokens to values.
* Returns array with keys: values (resolved token=>value) and unresolved (list of tokens not resolved / not allowed)
* Policy determines whether invalid tokens throw (fail) or are collected (blank|keep).
*
* @return array{values:array<string,string>,unresolved:array<int,string>,customTypes?:array<string,string>}
*/
public function resolve(
array $tokens,
DocumentTemplate $template,
Contract $contract,
User $user,
string $policy = 'fail',
array $customOverrides = [],
?array $customDefaults = null,
string $onMissingCustom = 'empty'
): array {
$values = [];
$unresolved = [];
$customTypesOut = [];
// Custom namespace: merge defaults from settings/template meta and overrides
$settings = app(\App\Services\Documents\DocumentSettings::class)->get();
$defaults = $customDefaults ?? ($template->meta['custom_defaults'] ?? null) ?? ($settings->custom_defaults ?? []);
if (! is_array($defaults)) {
$defaults = [];
}
if (! is_array($customOverrides)) {
$customOverrides = [];
}
$custom = array_replace($defaults, $customOverrides);
// Collect custom types from template meta (optional)
$customTypes = [];
if (isset($template->meta['custom_default_types']) && is_array($template->meta['custom_default_types'])) {
foreach ($template->meta['custom_default_types'] as $k => $t) {
$t = in_array($t, ['string', 'number', 'date'], true) ? $t : 'string';
$customTypes[(string) $k] = $t;
}
}
// Retrieve whitelist from DB settings (if present) and merge with config baseline (config acts as baseline; DB can add or override entity arrays)
$settingsWhitelist = app(\App\Services\Documents\DocumentSettings::class)->get()->whitelist ?? [];
$configWhitelist = config('documents.whitelist', []);
// Merge preserving DB additions/overrides
$globalWhitelist = array_replace($configWhitelist, $settingsWhitelist);
// Always treat globally whitelisted entities as available, even if legacy template does not list them
if ($template->entities && is_array($template->entities)) {
$templateEntities = array_values(array_unique(array_merge($template->entities, array_keys($globalWhitelist))));
} else {
$templateEntities = array_keys($globalWhitelist);
}
foreach ($tokens as $token) {
[$entity,$attr] = explode('.', $token, 2);
if ($entity === 'generation') {
$values[$token] = $this->generationAttribute($attr, $user);
continue;
}
if ($entity === 'custom') {
// Track type info if present
if (isset($customTypes[$attr])) {
$customTypesOut[$token] = $customTypes[$attr];
}
if (array_key_exists($attr, $custom)) {
$v = $custom[$attr];
if (is_scalar($v) || (is_object($v) && method_exists($v, '__toString'))) {
$values[$token] = (string) $v;
} else {
$values[$token] = '';
}
} else {
// Missing custom apply onMissingCustom policy locally (empty|leave|error)
if ($onMissingCustom === 'error') {
if ($policy === 'fail') {
throw new \RuntimeException("Manjkajoč custom token: {$token}");
}
$unresolved[] = $token;
} elseif ($onMissingCustom === 'leave') {
$unresolved[] = $token;
} else { // empty
$values[$token] = '';
}
}
continue;
}
if (! in_array($entity, $templateEntities, true)) {
if ($policy === 'fail') {
throw new \RuntimeException("Nedovoljen entiteta token: $entity");
}
$unresolved[] = $token;
continue;
}
// Allowed attributes: merge template-declared columns with global whitelist (config + DB settings)
// Rationale: old templates may not list newly allowed attributes (like nested paths),
// so we honor both sources instead of preferring one exclusively.
$allowedFromTemplate = $template->columns[$entity] ?? [];
$allowedFromGlobal = $globalWhitelist[$entity] ?? [];
$allowed = array_values(array_unique(array_merge($allowedFromTemplate, $allowedFromGlobal)));
if (! in_array($attr, $allowed, true)) {
if ($policy === 'fail') {
throw new \RuntimeException("Nedovoljen stolpec token: $token");
}
$unresolved[] = $token;
continue;
}
$values[$token] = $this->entityAttribute($entity, $attr, $contract) ?? '';
}
return [
'values' => $values,
'unresolved' => array_values(array_unique($unresolved)),
'customTypes' => $customTypesOut,
];
}
private function generationAttribute(string $attr, User $user): string
{
return match ($attr) {
'timestamp' => (string) now()->timestamp,
'date' => now()->toDateString(), // raw ISO; formatting applied later
'user_name' => $user->name ?? 'Uporabnik',
default => ''
};
}
private function entityAttribute(string $entity, string $attr, Contract $contract): ?string
{
switch ($entity) {
case 'contract':
return (string) ($contract->{$attr} ?? '');
case 'client_case':
return (string) optional($contract->clientCase)->{$attr};
case 'client':
$client = optional($contract->clientCase)->client;
if (! $client) {
return '';
}
if (str_contains($attr, '.')) {
return $this->resolveNestedFromModel($client, $attr);
}
return (string) ($client->{$attr} ?? '');
case 'person':
$person = optional($contract->clientCase)->person;
if (! $person) {
return '';
}
if (str_contains($attr, '.')) {
return $this->resolveNestedFromModel($person, $attr);
}
return (string) ($person->{$attr} ?? '');
case 'account':
$account = optional($contract->account);
return (string) $account->{$attr};
default:
return '';
}
}
/**
* Resolve nested dotted paths from a base model for supported relations/aliases.
* Supports:
* - Client: person.*
* - Person: person_address.* (uses first active address)
*/
private function resolveNestedFromModel(object $model, string $path): string
{
$segments = explode('.', $path);
$current = $model;
foreach ($segments as $seg) {
if (! $current) {
return '';
}
if ($current instanceof \App\Models\Client && $seg === 'person') {
$current = $current->person;
continue;
}
if ($current instanceof \App\Models\Person\Person && $seg === 'person_address') {
$current = $current->addresses()->first();
continue;
}
// Default attribute access
try {
$current = is_array($current) ? ($current[$seg] ?? null) : ($current->{$seg} ?? null);
} catch (\Throwable $e) {
return '';
}
}
return $current !== null ? (string) $current : '';
}
}