devsofttech_hms_ci4/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php

368 lines
11 KiB
PHP

<?php
namespace App\Libraries\Abdm\Fhir;
class FhirDocumentBuilder
{
/** @var array<int,array<string,mixed>> */
private array $entries = [];
/** @var array<string,bool> */
private array $resourceIds = [];
/** @var array<string,mixed> */
private array $bundleMeta = [];
/** @var array<string,mixed>|null */
private ?array $composition = null;
public function buildBundleMeta(string $identifierValue, string $timestamp): self
{
$this->bundleMeta = [
'resourceType' => 'Bundle',
'id' => $identifierValue,
'meta' => [
'versionId' => '1',
'lastUpdated' => $timestamp,
'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'],
'security' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality',
'code' => 'V',
'display' => 'very restricted'
]]
],
'identifier' => [
'system' => 'https://hms.local/fhir/document',
'value' => $identifierValue,
],
'type' => 'document',
'timestamp' => $timestamp,
];
return $this;
}
/** @param array<string,mixed> $values */
public function updateBundleMeta(array $values): self
{
if (isset($values['meta']) && is_array($values['meta']) && isset($this->bundleMeta['meta']) && is_array($this->bundleMeta['meta'])) {
$values['meta'] = array_replace($this->bundleMeta['meta'], $values['meta']);
}
$this->bundleMeta = array_replace($this->bundleMeta, $values);
return $this;
}
/**
* @param array<string,mixed> $composition
*/
public function buildComposition(array $composition): self
{
$this->composition = $composition;
return $this;
}
/** @param array<string,mixed> $values */
public function updateComposition(array $values): self
{
if (is_array($this->composition)) {
if (isset($values['meta']) && is_array($values['meta']) && isset($this->composition['meta']) && is_array($this->composition['meta'])) {
$values['meta'] = array_replace($this->composition['meta'], $values['meta']);
}
$this->composition = array_replace($this->composition, $values);
}
return $this;
}
/**
* @param array<string,mixed> $resource
*/
public function addPatient(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addEncounter(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addPractitioner(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addOrganization(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addCondition(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addObservation(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addServiceRequest(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addAllergyIntolerance(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addCarePlan(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addDiagnosticReport(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addMedicationRequest(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addProcedure(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addClaim(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addDocumentReference(array $resource): self
{
return $this->addResource($resource);
}
/** @param array<string,mixed> $resource */
public function addImmunization(array $resource): self
{
return $this->addResource($resource);
}
/**
* @return array<string,mixed>
*/
public function toBundle(): array
{
$bundle = $this->bundleMeta;
$entries = [];
if (is_array($this->composition)) {
$entries[] = $this->wrapEntry($this->composition);
}
foreach ($this->entries as $entry) {
$entries[] = $entry;
}
$bundle['entry'] = $this->normalizeEntryReferences($entries);
return $this->stripNulls($bundle);
}
/**
* A urn:uuid fullUrl must contain an RFC 4122 UUID. Resource ids remain
* human-readable while all document references use deterministic UUID URNs.
*
* @param array<int,array<string,mixed>> $entries
* @return array<int,array<string,mixed>>
*/
private function normalizeEntryReferences(array $entries): array
{
$referenceMap = [];
$idMap = [];
foreach ($entries as $entry) {
$resource = (array) ($entry['resource'] ?? []);
$id = (string) ($resource['id'] ?? '');
if ($id === '') {
continue;
}
$newUuid = $this->uuidForResource($resource);
$referenceMap['urn:uuid:' . $id] = 'urn:uuid:' . $newUuid;
$idMap[$id] = $newUuid;
}
foreach ($entries as &$entry) {
$resource = (array) ($entry['resource'] ?? []);
$id = (string) ($resource['id'] ?? '');
if ($id !== '') {
$entry['fullUrl'] = $referenceMap['urn:uuid:' . $id];
}
$resource = $this->rewriteReferences($resource, $referenceMap);
if ($id !== '') {
$resource['id'] = $idMap[$id];
}
$entry['resource'] = $resource;
}
unset($entry);
return $entries;
}
/** @param array<string,mixed> $resource */
private function uuidForResource(array $resource): string
{
$hex = md5((string) ($resource['resourceType'] ?? 'Resource') . '/' . (string) ($resource['id'] ?? ''));
return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-4' . substr($hex, 13, 3)
. '-a' . substr($hex, 17, 3) . '-' . substr($hex, 20, 12);
}
/**
* @param mixed $value
* @param array<string,string> $referenceMap
* @return mixed
*/
private function rewriteReferences($value, array $referenceMap)
{
if (is_string($value)) {
return $referenceMap[$value] ?? $value;
}
if (! is_array($value)) {
return $value;
}
foreach ($value as $key => $nested) {
$value[$key] = $this->rewriteReferences($nested, $referenceMap);
}
return $value;
}
/**
* @param array<string,mixed> $resource
*/
public function addResource(array $resource): self
{
$id = (string) ($resource['id'] ?? '');
if ($id === '') {
$resource['id'] = $this->newId((string) ($resource['resourceType'] ?? 'resource'));
$id = (string) $resource['id'];
}
$fullRef = (string) ($resource['resourceType'] ?? '') . '/' . $id;
if (isset($this->resourceIds[$fullRef])) {
return $this;
}
$this->resourceIds[$fullRef] = true;
$this->entries[] = $this->wrapEntry($resource);
return $this;
}
/**
* @param array<string,mixed> $resource
* @return array<string,mixed>
*/
private function wrapEntry(array $resource): array
{
$id = (string) ($resource['id'] ?? $this->newId((string) ($resource['resourceType'] ?? 'resource')));
$resource['id'] = $id;
return [
'fullUrl' => 'urn:uuid:' . $id,
'resource' => $this->stripNulls($resource),
];
}
/**
* @param array<string,mixed> $data
* @return array<string,mixed>
*/
private function stripNulls(array $data): array
{
$out = [];
foreach ($data as $key => $value) {
if ($value === null || $value === '' || $this->isPlaceholderValue($value)) {
continue;
}
if (is_array($value)) {
$clean = $this->stripNested($value);
if ($clean === [] || $clean === null) {
continue;
}
$out[$key] = $clean;
continue;
}
$out[$key] = $value;
}
return $out;
}
/**
* @param array<int|string,mixed> $value
* @return array<int|string,mixed>|null
*/
private function stripNested(array $value): ?array
{
$clean = [];
foreach ($value as $k => $v) {
if ($v === null || $v === '' || $this->isPlaceholderValue($v)) {
continue;
}
if (is_array($v)) {
$nested = $this->stripNested($v);
if ($nested === null || $nested === []) {
continue;
}
$clean[$k] = $nested;
continue;
}
$clean[$k] = $v;
}
return $clean === [] ? null : $clean;
}
/**
* Returns true only for display-level placeholder strings that should never
* appear in a rendered FHIR bundle (pure "no data" sentinel values).
* NOTE: '0' is intentionally excluded — it is a valid FHIR quantity value,
* coded value, and identifier string. Its earlier inclusion caused silent
* data loss in observation valueQuantity and numeric identifiers.
*
* @param mixed $value
*/
private function isPlaceholderValue($value): bool
{
if (! is_string($value)) {
return false;
}
return in_array(strtoupper(trim($value)), ['NA', 'N/A', 'N / A', 'NULL', 'NOT AVAILABLE', 'UNDEFINED'], true);
}
private function newId(string $prefix): string
{
$prefix = strtolower(preg_replace('/[^a-z0-9]/i', '-', $prefix) ?: 'resource');
$bytes = bin2hex(random_bytes(6));
return $prefix . '-' . $bytes;
}
}