get($template->file_path); // Work in temp file $tmpIn = tempnam(sys_get_temp_dir(), 'tmpl'); file_put_contents($tmpIn, $templateStream); $zip = new ZipArchive; $zip->open($tmpIn); $docXml = $zip->getFromName('word/document.xml'); if ($docXml === false) { throw new \RuntimeException('Manjkajoča document.xml'); } $tokens = $this->scanner->scan($docXml); // Determine effective unresolved policy early (template override -> global -> config) $globalSettingsEarly = app(\App\Services\Documents\DocumentSettings::class)->get(); $effectivePolicy = $template->fail_on_unresolved ? 'fail' : ($globalSettingsEarly->unresolved_policy ?? config('documents.unresolved_policy', 'fail')); // Resolve with support for custom.* tokens: per-generation overrides and defaults from template meta or global settings. $customOverrides = request()->input('custom', []); // if called via HTTP context; otherwise pass explicitly from caller $customDefaults = is_array($template->meta['custom_defaults'] ?? null) ? $template->meta['custom_defaults'] : null; $resolved = $this->resolver->resolve( $tokens, $template, $contract, $user, $effectivePolicy, is_array($customOverrides) ? $customOverrides : [], $customDefaults, 'empty' ); $values = $resolved['values']; $initialUnresolved = $resolved['unresolved']; $customTypes = $resolved['customTypes'] ?? []; // Formatting options $fmt = $template->formatting_options ?? []; $decimals = (int) ($fmt['number_decimals'] ?? 2); $decSep = $fmt['decimal_separator'] ?? '.'; $thouSep = $fmt['thousands_separator'] ?? ','; $currencySymbol = $fmt['currency_symbol'] ?? null; $currencyPos = $fmt['currency_position'] ?? 'before'; $currencySpace = (bool) ($fmt['currency_space'] ?? false); $globalSettings = app(\App\Services\Documents\DocumentSettings::class)->get(); $globalDateFormats = $globalSettings->date_formats ?? []; foreach ($values as $k => $v) { $isTypedDate = ($customTypes[$k] ?? null) === 'date'; $isTypedNumber = ($customTypes[$k] ?? null) === 'number'; // Date formatting (typed or heuristic based on key ending with _date or .date) if (is_string($v) && ($isTypedDate || $k === 'generation.date' || preg_match('/(^|\.)[A-Za-z_]*date$/i', $k))) { $dateFmtOverrides = $fmt['date_formats'] ?? []; $desiredFormat = $dateFmtOverrides[$k] ?? ($globalDateFormats[$k] ?? null) ?? ($fmt['default_date_format'] ?? null) ?? ($template->date_format ?: null) ?? ($globalSettings->date_format ?? null) ?? config('documents.date_format', 'Y-m-d'); if ($desiredFormat) { try { $dt = Carbon::parse($v); $values[$k] = $dt->format($desiredFormat); continue; // skip numeric detection below } catch (\Throwable $e) { // swallow } } } // Number formatting: only for explicitly typed numbers or common monetary fields $isFinanceField = (bool) preg_match('/(^|\.)\b(amount|balance|total|price|cost)\b$/i', $k); if (($isTypedNumber || $isFinanceField) && is_numeric($v)) { $num = number_format((float) $v, $decimals, $decSep, $thouSep); if ($currencySymbol && $isFinanceField) { $space = $currencySpace ? ' ' : ''; if ($currencyPos === 'after') { $num = $num.$space.$currencySymbol; } else { $num = $currencySymbol.$space.$num; } } $values[$k] = $num; } } // Replace tokens foreach ($values as $token => $val) { $docXml = str_replace('{{'.$token.'}}', htmlspecialchars($val), $docXml); } // After replacement: check unresolved patterns if (! empty($initialUnresolved)) { if ($effectivePolicy === 'blank') { foreach ($initialUnresolved as $r) { $docXml = str_replace('{{'.$r.'}}', '', $docXml); } } elseif ($effectivePolicy === 'keep') { // keep unresolved markers } else { // fail throw new UnresolvedTokensException($initialUnresolved, 'Neuspešna zamenjava tokenov'); } } $zip->addFromString('word/document.xml', $docXml); $zip->close(); $output = file_get_contents($tmpIn); $checksum = hash('sha256', $output); $size = strlen($output); // Filename pattern & date format precedence: template override -> global settings -> config fallback $globalSettings = $globalSettings ?? app(\App\Services\Documents\DocumentSettings::class)->get(); $pattern = $template->output_filename_pattern ?: ($globalSettings->file_name_pattern ?? config('documents.file_name_pattern')); $dateFormat = $template->date_format ?: ($globalSettings->date_format ?? config('documents.date_format', 'Y-m-d')); $replacements = [ '{slug}' => $template->slug, '{version}' => 'v'.$template->version, '{generation.date}' => now()->format($dateFormat), '{generation.timestamp}' => (string) now()->timestamp, ]; // Also allow any token ({{x.y}}) style replaced pattern variants: convert {contract.reference} foreach ($values as $token => $val) { $replacements['{'.$token.'}'] = Str::slug((string) $val) ?: 'value'; } $fileName = strtr($pattern, $replacements); if (! str_ends_with(strtolower($fileName), '.docx')) { $fileName .= '.docx'; } $relativeDir = 'contracts/'.$contract->uuid.'/generated/'.now()->toDateString(); $relativePath = $relativeDir.'/'.$fileName; Storage::disk($disk)->put($relativePath, $output); return [ 'fileName' => $fileName, 'relativePath' => $relativePath, 'size' => $size, 'checksum' => $checksum, ]; } }