netgescon-day0/app/Services/FattureElettroniche/FeXsdCodesImporter.php

197 lines
7.4 KiB
PHP

<?php
namespace App\Services\FattureElettroniche;
use App\Models\FeXsdCode;
use App\Models\FeXsdCodeDomain;
use App\Models\FeXsdVersion;
use App\Models\User;
use DOMDocument;
use DOMXPath;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Storage;
class FeXsdCodesImporter
{
/**
* Importa tutte le enumerazioni (simpleType/restriction/enumeration) da un file XSD.
*
* @return array{version: FeXsdVersion, summary: array<string,mixed>}
*/
public function importFromUploadedXsd(
string $tempPath,
?string $versionLabel,
?string $note,
?User $createdBy
): array {
$content = Storage::disk('local')->get($tempPath);
$sha256 = hash('sha256', $content);
$existing = FeXsdVersion::query()->where('sha256', $sha256)->first();
if ($existing) {
// File già importato: pulisco il temporaneo e ritorno la versione esistente.
try {
Storage::disk('local')->delete($tempPath);
} catch (\Throwable) {
// ignore
}
return [
'version' => $existing,
'summary' => [
'already_imported' => true,
'sha256' => $sha256,
],
];
}
$originalFilename = basename($tempPath);
$finalPath = 'fe/xsd/' . $sha256 . '.xsd';
return DB::transaction(function () use ($content, $sha256, $tempPath, $finalPath, $originalFilename, $versionLabel, $note, $createdBy): array {
// Sposto/normalizzo il path del file appena possibile (se fallisce, andiamo avanti).
try {
if (!Storage::disk('local')->exists($finalPath)) {
Storage::disk('local')->move($tempPath, $finalPath);
} else {
Storage::disk('local')->delete($tempPath);
}
} catch (\Throwable) {
// ignore
}
$parsed = $this->parseXsdEnumerations($content);
$version = FeXsdVersion::query()->create([
'version_label' => $versionLabel ? trim($versionLabel) : null,
'original_filename' => $originalFilename,
'stored_path' => $finalPath,
'sha256' => $sha256,
'created_by_user_id' => $createdBy?->id,
'imported_at' => now(),
'note' => $note ? trim($note) : null,
'meta' => [
'domains' => count($parsed['domains']),
'codes' => $parsed['codes_count'],
],
]);
foreach ($parsed['domains'] as $domain) {
$domainRow = FeXsdCodeDomain::query()->create([
'version_id' => $version->id,
'type_name' => (string) $domain['type_name'],
'base_type' => Arr::get($domain, 'base_type'),
'documentation' => Arr::get($domain, 'documentation'),
'codes_count' => count($domain['codes'] ?? []),
]);
$i = 0;
foreach (($domain['codes'] ?? []) as $codeRow) {
$i++;
FeXsdCode::query()->create([
'domain_id' => $domainRow->id,
'code' => (string) ($codeRow['code'] ?? ''),
'label' => Arr::get($codeRow, 'label'),
'documentation' => Arr::get($codeRow, 'documentation'),
'sort_order' => $i,
]);
}
}
return [
'version' => $version,
'summary' => [
'already_imported' => false,
'sha256' => $sha256,
'domains' => count($parsed['domains']),
'codes' => $parsed['codes_count'],
],
];
});
}
/**
* @return array{domains: array<int,array{type_name:string,base_type:?string,documentation:?string,codes:array<int,array{code:string,label:?string,documentation:?string}>}>, codes_count:int}
*/
private function parseXsdEnumerations(string $content): array
{
$doc = new DOMDocument();
$doc->preserveWhiteSpace = false;
$doc->formatOutput = false;
$ok = @$doc->loadXML($content, LIBXML_NONET | LIBXML_NOERROR | LIBXML_NOWARNING);
if (!$ok) {
throw new \RuntimeException('XSD non valido o non leggibile.');
}
$xpath = new DOMXPath($doc);
// Trova simpleType con name e con almeno una enumeration.
$simpleTypes = $xpath->query("//*[local-name()='simpleType'][@name][.//*[local-name()='enumeration']]");
$domains = [];
$codesCount = 0;
if ($simpleTypes) {
foreach ($simpleTypes as $simpleTypeNode) {
$typeName = (string) $simpleTypeNode->attributes?->getNamedItem('name')?->nodeValue;
$typeName = trim($typeName);
if ($typeName === '') {
continue;
}
// baseType dal restriction base="..."
$baseTypeNode = $xpath->query(".//*[local-name()='restriction'][@base]", $simpleTypeNode)?->item(0);
$baseType = $baseTypeNode?->attributes?->getNamedItem('base')?->nodeValue;
$baseType = is_string($baseType) ? trim($baseType) : null;
$docTextNode = $xpath->query(".//*[local-name()='annotation']/*[local-name()='documentation']", $simpleTypeNode)?->item(0);
$documentation = $docTextNode ? trim((string) $docTextNode->textContent) : null;
$documentation = $documentation !== '' ? $documentation : null;
$enumNodes = $xpath->query(".//*[local-name()='restriction']/*[local-name()='enumeration'][@value]", $simpleTypeNode);
if (!$enumNodes || $enumNodes->length === 0) {
continue;
}
$codes = [];
foreach ($enumNodes as $enumNode) {
$value = (string) $enumNode->attributes?->getNamedItem('value')?->nodeValue;
$value = trim($value);
if ($value === '') {
continue;
}
$enumDocNode = $xpath->query(".//*[local-name()='annotation']/*[local-name()='documentation']", $enumNode)?->item(0);
$enumDoc = $enumDocNode ? trim((string) $enumDocNode->textContent) : null;
$enumDoc = $enumDoc !== '' ? $enumDoc : null;
$codes[] = [
'code' => $value,
'label' => null,
'documentation' => $enumDoc,
];
$codesCount++;
}
$domains[] = [
'type_name' => $typeName,
'base_type' => $baseType,
'documentation' => $documentation,
'codes' => $codes,
];
}
}
// Ordina stabilmente per nome per evitare fluttuazioni tra import.
usort($domains, fn(array $a, array $b) => strcmp((string) $a['type_name'], (string) $b['type_name']));
return [
'domains' => $domains,
'codes_count' => $codesCount,
];
}
}