diff --git a/app/Support/Nmr/DatasetSpectraInfoExtractor.php b/app/Support/Nmr/DatasetSpectraInfoExtractor.php index b70c56b7..775a0f88 100644 --- a/app/Support/Nmr/DatasetSpectraInfoExtractor.php +++ b/app/Support/Nmr/DatasetSpectraInfoExtractor.php @@ -156,15 +156,12 @@ private function buildSearchText(array $info): string $parts = []; foreach ($info as $key => $value) { - if ($value === null || $value === '') { + $flattened = $this->flattenForSearch($value); + if ($flattened === '') { continue; } - if (is_array($value)) { - $parts[] = $key.' '.implode(' ', array_map('strval', $value)); - } else { - $parts[] = $key.' '.(string) $value; - } + $parts[] = $key.' '.$flattened; } $parts[] = json_encode($info, JSON_UNESCAPED_UNICODE) ?: ''; @@ -174,6 +171,39 @@ private function buildSearchText(array $info): string return $normalized ?? ''; } + /** + * Recursively stringify scalars so nested NMRium arrays (2D frequencies, + * filters, meta) never trigger "Array to string conversion". + */ + private function flattenForSearch(mixed $value): string + { + if ($value === null || $value === '') { + return ''; + } + + if (is_bool($value)) { + return $value ? '1' : ''; + } + + if (is_array($value)) { + $parts = []; + foreach ($value as $item) { + $flattened = $this->flattenForSearch($item); + if ($flattened !== '') { + $parts[] = $flattened; + } + } + + return implode(' ', $parts); + } + + if (! is_scalar($value)) { + return ''; + } + + return trim((string) $value); + } + private function property(array $info, string $key): mixed { return $info[$key] ?? null; @@ -185,20 +215,56 @@ private function normalizeString(mixed $value): ?string return null; } + if (is_array($value)) { + foreach ($this->scalarLeaves($value) as $leaf) { + $normalized = $this->normalizeString($leaf); + if ($normalized !== null) { + return $normalized; + } + } + + return null; + } + + if (is_bool($value) || ! is_scalar($value)) { + return null; + } + $string = trim((string) $value); return $string === '' ? null : $string; } + /** + * @return list + */ + private function scalarLeaves(array $value): array + { + $leaves = []; + + array_walk_recursive($value, function (mixed $item) use (&$leaves): void { + if (is_scalar($item)) { + $leaves[] = $item; + } + }); + + return $leaves; + } + private function normalizeNucleus(mixed $value): ?string + { + return $this->normalizeString($value); + } + + private function normalizeDecimal(mixed $value): ?string { if ($value === null || $value === '') { return null; } if (is_array($value)) { - foreach ($value as $item) { - $normalized = $this->normalizeString($item); + foreach ($this->scalarLeaves($value) as $leaf) { + $normalized = $this->normalizeDecimal($leaf); if ($normalized !== null) { return $normalized; } @@ -207,15 +273,6 @@ private function normalizeNucleus(mixed $value): ?string return null; } - return $this->normalizeString($value); - } - - private function normalizeDecimal(mixed $value): ?string - { - if ($value === null || $value === '') { - return null; - } - if (! is_numeric($value)) { return null; } @@ -229,6 +286,17 @@ private function normalizeInteger(mixed $value): ?int return null; } + if (is_array($value)) { + foreach ($this->scalarLeaves($value) as $leaf) { + $normalized = $this->normalizeInteger($leaf); + if ($normalized !== null) { + return $normalized; + } + } + + return null; + } + if (! is_numeric($value)) { return null; } @@ -245,7 +313,7 @@ private function normalizeSmallInteger(mixed $value): ?int private function normalizeBoolean(mixed $value): ?bool { - if ($value === null || $value === '') { + if ($value === null || $value === '' || is_array($value)) { return null; } @@ -257,6 +325,10 @@ private function normalizeBoolean(mixed $value): ?bool return (bool) $value; } + if (! is_scalar($value)) { + return null; + } + $normalized = strtolower(trim((string) $value)); return match ($normalized) { @@ -365,10 +437,25 @@ private function normalizeTubeDiameter(mixed $value): ?string return null; } + if (is_array($value)) { + foreach ($this->scalarLeaves($value) as $leaf) { + $normalized = $this->normalizeTubeDiameter($leaf); + if ($normalized !== null) { + return $normalized; + } + } + + return null; + } + if (is_numeric($value)) { return (string) (int) round((float) $value); } + if (! is_scalar($value)) { + return null; + } + $string = strtolower(trim((string) $value)); if (preg_match('/(\d+(?:\.\d+)?)\s*mm?/', $string, $matches) === 1) { diff --git a/tests/Unit/Support/DatasetSpectraInfoExtractorTest.php b/tests/Unit/Support/DatasetSpectraInfoExtractorTest.php index 0e307a24..4026fbd0 100644 --- a/tests/Unit/Support/DatasetSpectraInfoExtractorTest.php +++ b/tests/Unit/Support/DatasetSpectraInfoExtractorTest.php @@ -303,6 +303,57 @@ public function test_extract_falls_back_to_jcamp_when_no_vendor_metadata(): void $this->assertSame('JCAMP', $payload['spectra_manufacturer']); } + public function test_extract_handles_nested_arrays_without_array_to_string_error(): void + { + $dataset = $this->makeDataset(); + + NMRium::factory()->forDataset($dataset)->create([ + 'nmrium_info' => [ + 'data' => [ + 'spectra' => [ + [ + 'info' => [ + 'solvent' => 'CDCl3', + 'nucleus' => ['1H', '13C'], + 'experiment' => ['HSQC'], + 'probeName' => ['name' => 'BBO'], + 'originFrequency' => [600.13, 150.9], + 'baseFrequency' => [600.13, 150.9], + 'spectralWidth' => [16.02, 220.0], + 'numberOfPoints' => [2048, 512], + 'filters' => [ + [ + 'name' => 'shift2D', + 'value' => ['shift' => [0, 0]], + ], + ], + 'meta' => [ + 'instrument' => ['vendor' => 'Bruker'], + ], + ], + ], + ], + ], + ], + ]); + + $dataset->refresh(); + + $payload = $this->extractor->extractForDataset($dataset); + + $this->assertSame('CDCl3', $payload['spectra_solvent']); + $this->assertSame('1H', $payload['spectra_nucleus']); + $this->assertSame('HSQC', $payload['spectra_experiment']); + $this->assertSame('BBO', $payload['spectra_probe_name']); + $this->assertSame('600.13', $payload['spectra_origin_frequency']); + $this->assertSame('600.13', $payload['spectra_base_frequency']); + $this->assertSame('16.02', $payload['spectra_spectral_width']); + $this->assertSame(2048, $payload['spectra_number_of_points']); + $this->assertNotNull($payload['spectra_info_extracted_at']); + $this->assertStringContainsString('bruker', strtolower($payload['spectra_search_text'])); + $this->assertStringContainsString('hsqc', $payload['spectra_search_text']); + } + public function test_extract_returns_empty_payload_when_no_nmrium_info(): void { $dataset = $this->makeDataset();