mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 19:19:29 +00:00
Doing refactoring to remove remains of arrays
This commit is contained in:
parent
98b62cc81e
commit
27a18bdc1e
10 changed files with 341 additions and 443 deletions
|
|
@ -29,7 +29,9 @@ use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
use App\Form\InfoProviderSystem\GlobalFieldMappingType;
|
||||||
use App\Services\InfoProviderSystem\BulkInfoProviderService;
|
use App\Services\InfoProviderSystem\BulkInfoProviderService;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\PartSearchResultsDTO;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Psr\Log\LoggerInterface;
|
use Psr\Log\LoggerInterface;
|
||||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||||
|
|
@ -99,25 +101,20 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
return $job;
|
return $job;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function updatePartSearchResults(BulkInfoProviderImportJob $job, int $partId, ?array $newResults): void
|
private function updatePartSearchResults(BulkInfoProviderImportJob $job, int $partId, ?PartSearchResultsDTO $newResults): void
|
||||||
{
|
{
|
||||||
if ($newResults === null) {
|
if ($newResults === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only deserialize and update if we have new results
|
// Only deserialize and update if we have new results
|
||||||
$allResults = $job->deserializeSearchResults($this->entityManager);
|
$allResults = $job->getSearchResults($this->entityManager);
|
||||||
|
|
||||||
// Find and update the results for this specific part
|
// Find and update the results for this specific part
|
||||||
foreach ($allResults as $index => $partResult) {
|
$allResults = $allResults->replaceResultsForPart($partId, $newResults);
|
||||||
if ($partResult['part']->getId() === $partId) {
|
|
||||||
$allResults[$index] = $newResults;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save updated results back to job
|
// Save updated results back to job
|
||||||
$job->setSearchResults($job->serializeSearchResults($allResults));
|
$job->setSearchResults($allResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Route('/step1', name: 'bulk_info_provider_step1')]
|
#[Route('/step1', name: 'bulk_info_provider_step1')]
|
||||||
|
|
@ -219,17 +216,14 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
$fieldMappingDtos = $this->convertFieldMappingsToDto($fieldMappings);
|
$fieldMappingDtos = $this->convertFieldMappingsToDto($fieldMappings);
|
||||||
$searchResultsDto = $this->bulkService->performBulkSearch($parts, $fieldMappingDtos, $prefetchDetails);
|
$searchResultsDto = $this->bulkService->performBulkSearch($parts, $fieldMappingDtos, $prefetchDetails);
|
||||||
|
|
||||||
// Convert DTO back to array format for legacy compatibility
|
|
||||||
$searchResults = $searchResultsDto->toArray();
|
|
||||||
|
|
||||||
// Save search results to job
|
// Save search results to job
|
||||||
$job->setSearchResults($job->serializeSearchResults($searchResults));
|
$job->setSearchResults($searchResultsDto);
|
||||||
$job->markAsInProgress();
|
$job->markAsInProgress();
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
// Prefetch details if requested
|
// Prefetch details if requested
|
||||||
if ($prefetchDetails) {
|
if ($prefetchDetails) {
|
||||||
$this->bulkService->prefetchDetailsForResults($searchResults);
|
$this->bulkService->prefetchDetailsForResults($searchResultsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $job->getId()]);
|
return $this->redirectToRoute('bulk_info_provider_step2', ['jobId' => $job->getId()]);
|
||||||
|
|
@ -369,7 +363,7 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
|
|
||||||
// Get the parts and deserialize search results
|
// Get the parts and deserialize search results
|
||||||
$parts = $job->getJobParts()->map(fn($jobPart) => $jobPart->getPart())->toArray();
|
$parts = $job->getJobParts()->map(fn($jobPart) => $jobPart->getPart())->toArray();
|
||||||
$searchResults = $job->deserializeSearchResults($this->entityManager);
|
$searchResults = $job->getSearchResults($this->entityManager);
|
||||||
|
|
||||||
return $this->render('info_providers/bulk_import/step2.html.twig', [
|
return $this->render('info_providers/bulk_import/step2.html.twig', [
|
||||||
'job' => $job,
|
'job' => $job,
|
||||||
|
|
@ -481,34 +475,33 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails);
|
$searchResultsDto = $this->bulkService->performBulkSearch([$part], $fieldMappingDtos, $prefetchDetails);
|
||||||
$searchResults = $searchResultsDto->toArray();
|
|
||||||
} catch (\Exception $searchException) {
|
} catch (\Exception $searchException) {
|
||||||
// Handle "no search results found" as a normal case, not an error
|
// Handle "no search results found" as a normal case, not an error
|
||||||
if (str_contains($searchException->getMessage(), 'No search results found')) {
|
if (str_contains($searchException->getMessage(), 'No search results found')) {
|
||||||
$searchResults = [];
|
$searchResultsDto = null;
|
||||||
} else {
|
} else {
|
||||||
throw $searchException;
|
throw $searchException;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the job's search results for this specific part efficiently
|
// Update the job's search results for this specific part efficiently
|
||||||
$this->updatePartSearchResults($job, $partId, $searchResults[0] ?? null);
|
$this->updatePartSearchResults($job, $partId, $searchResultsDto[0] ?? null);
|
||||||
|
|
||||||
// Prefetch details if requested
|
// Prefetch details if requested
|
||||||
if ($prefetchDetails && !empty($searchResults)) {
|
if ($prefetchDetails && $searchResultsDto !== null) {
|
||||||
$this->bulkService->prefetchDetailsForResults($searchResults);
|
$this->bulkService->prefetchDetailsForResults($searchResultsDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
// Return the new results for this part
|
// Return the new results for this part
|
||||||
$newResults = $searchResults[0] ?? null;
|
$newResults = $searchResultsDto[0] ?? null;
|
||||||
|
|
||||||
return $this->json([
|
return $this->json([
|
||||||
'success' => true,
|
'success' => true,
|
||||||
'part_id' => $partId,
|
'part_id' => $partId,
|
||||||
'results_count' => $newResults ? count($newResults['search_results']) : 0,
|
'results_count' => $newResults ? $newResults->getResultCount() : 0,
|
||||||
'errors_count' => $newResults ? count($newResults['errors']) : 0,
|
'errors_count' => $newResults ? $newResults->getErrorCount() : 0,
|
||||||
'message' => 'Part research completed successfully'
|
'message' => 'Part research completed successfully'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
@ -555,13 +548,12 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
$prefetchDetails = $job->isPrefetchDetails();
|
$prefetchDetails = $job->isPrefetchDetails();
|
||||||
|
|
||||||
// Process in batches to reduce memory usage for large operations
|
// Process in batches to reduce memory usage for large operations
|
||||||
$allResults = [];
|
$allResults = new BulkSearchResponseDTO(partResults: []);
|
||||||
$batches = array_chunk($parts, $this->bulkImportBatchSize);
|
$batches = array_chunk($parts, $this->bulkImportBatchSize);
|
||||||
|
|
||||||
foreach ($batches as $batch) {
|
foreach ($batches as $batch) {
|
||||||
$batchResultsDto = $this->bulkService->performBulkSearch($batch, $fieldMappingDtos, $prefetchDetails);
|
$batchResultsDto = $this->bulkService->performBulkSearch($batch, $fieldMappingDtos, $prefetchDetails);
|
||||||
$batchResults = $batchResultsDto->toArray();
|
$allResults = BulkSearchResponseDTO::merge($allResults, $batchResultsDto);
|
||||||
$allResults = array_merge($allResults, $batchResults);
|
|
||||||
|
|
||||||
// Properly manage entity manager memory without losing state
|
// Properly manage entity manager memory without losing state
|
||||||
$jobId = $job->getId();
|
$jobId = $job->getId();
|
||||||
|
|
@ -570,7 +562,7 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the job's search results
|
// Update the job's search results
|
||||||
$job->setSearchResults($job->serializeSearchResults($allResults));
|
$job->setSearchResults($allResults);
|
||||||
|
|
||||||
// Prefetch details if requested
|
// Prefetch details if requested
|
||||||
if ($prefetchDetails) {
|
if ($prefetchDetails) {
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ namespace App\Entity;
|
||||||
use App\Entity\Base\AbstractDBElement;
|
use App\Entity\Base\AbstractDBElement;
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\UserSystem\User;
|
use App\Entity\UserSystem\User;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||||
use Doctrine\Common\Collections\ArrayCollection;
|
use Doctrine\Common\Collections\ArrayCollection;
|
||||||
use Doctrine\Common\Collections\Collection;
|
use Doctrine\Common\Collections\Collection;
|
||||||
|
|
@ -45,6 +46,11 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
#[ORM\Column(type: Types::JSON)]
|
#[ORM\Column(type: Types::JSON)]
|
||||||
private array $searchResults = [];
|
private array $searchResults = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var BulkSearchResponseDTO|null The deserialized search results DTO, cached for performance
|
||||||
|
*/
|
||||||
|
private ?BulkSearchResponseDTO $searchResultsDTO = null;
|
||||||
|
|
||||||
#[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportJobStatus::class)]
|
#[ORM\Column(type: Types::STRING, length: 20, enumType: BulkImportJobStatus::class)]
|
||||||
private BulkImportJobStatus $status = BulkImportJobStatus::PENDING;
|
private BulkImportJobStatus $status = BulkImportJobStatus::PENDING;
|
||||||
|
|
||||||
|
|
@ -155,17 +161,33 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getSearchResults(): array
|
public function getSearchResultsRaw(): array
|
||||||
{
|
{
|
||||||
return $this->searchResults;
|
return $this->searchResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function setSearchResults(array $searchResults): self
|
public function setSearchResultsRaw(array $searchResults): self
|
||||||
{
|
{
|
||||||
$this->searchResults = $searchResults;
|
$this->searchResults = $searchResults;
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function setSearchResults(BulkSearchResponseDTO $searchResponse): self
|
||||||
|
{
|
||||||
|
$this->searchResultsDTO = $searchResponse;
|
||||||
|
$this->searchResults = $searchResponse->toSerializableRepresentation();
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSearchResults(EntityManagerInterface $entityManager): BulkSearchResponseDTO
|
||||||
|
{
|
||||||
|
if ($this->searchResultsDTO === null) {
|
||||||
|
// Lazy load the DTO from the raw JSON data
|
||||||
|
$this->searchResultsDTO = BulkSearchResponseDTO::fromSerializableRepresentation($this->searchResults, $entityManager);
|
||||||
|
}
|
||||||
|
return $this->searchResultsDTO;
|
||||||
|
}
|
||||||
|
|
||||||
public function getStatus(): BulkImportJobStatus
|
public function getStatus(): BulkImportJobStatus
|
||||||
{
|
{
|
||||||
return $this->status;
|
return $this->status;
|
||||||
|
|
@ -396,107 +418,4 @@ class BulkInfoProviderImportJob extends AbstractDBElement
|
||||||
$completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
|
$completed = $this->getCompletedPartsCount() + $this->getSkippedPartsCount();
|
||||||
return $completed >= $total;
|
return $completed >= $total;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param array $searchResults
|
|
||||||
* @return array{part_id: int, search_results: array{dto: array{provider_key: string, provider_id: string, name: string, description: string, manufacturer: string, mpn: string, provider_url: string, preview_image_url: string, _source_field: string|null, _source_keyword: string|null}, localPart: int|null}[], errors: string[]}[]
|
|
||||||
*/
|
|
||||||
public function serializeSearchResults(array $searchResults): array
|
|
||||||
{
|
|
||||||
$serialized = [];
|
|
||||||
|
|
||||||
foreach ($searchResults as $partResult) {
|
|
||||||
$partData = [
|
|
||||||
'part_id' => $partResult['part']->getId(),
|
|
||||||
'search_results' => [],
|
|
||||||
'errors' => $partResult['errors'] ?? []
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($partResult['search_results'] as $result) {
|
|
||||||
$dto = $result['dto'];
|
|
||||||
$partData['search_results'][] = [
|
|
||||||
'dto' => [
|
|
||||||
'provider_key' => $dto->provider_key,
|
|
||||||
'provider_id' => $dto->provider_id,
|
|
||||||
'name' => $dto->name,
|
|
||||||
'description' => $dto->description,
|
|
||||||
'manufacturer' => $dto->manufacturer,
|
|
||||||
'mpn' => $dto->mpn,
|
|
||||||
'provider_url' => $dto->provider_url,
|
|
||||||
'preview_image_url' => $dto->preview_image_url,
|
|
||||||
'_source_field' => $result['source_field'] ?? null,
|
|
||||||
'_source_keyword' => $result['source_keyword'] ?? null,
|
|
||||||
],
|
|
||||||
'localPart' => $result['localPart'] ? $result['localPart']->getId() : null
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$serialized[] = $partData;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $serialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param EntityManagerInterface|null $entityManager
|
|
||||||
* @return array{part: Part, search_results: array{dto: SearchResultDTO, localPart: Part|null, source_field: string|null, source_keyword: string|null}[], errors: string[]}[]
|
|
||||||
*/
|
|
||||||
public function deserializeSearchResults(?EntityManagerInterface $entityManager = null): array
|
|
||||||
{
|
|
||||||
if (empty($this->searchResults)) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
$parts = $this->jobParts->map(fn($jobPart) => $jobPart->getPart())->toArray();
|
|
||||||
$partsById = [];
|
|
||||||
foreach ($parts as $part) {
|
|
||||||
$partsById[$part->getId()] = $part;
|
|
||||||
}
|
|
||||||
|
|
||||||
$searchResults = [];
|
|
||||||
|
|
||||||
foreach ($this->searchResults as $partData) {
|
|
||||||
$part = $partsById[$partData['part_id']] ?? null;
|
|
||||||
if (!$part) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$partResult = [
|
|
||||||
'part' => $part,
|
|
||||||
'search_results' => [],
|
|
||||||
'errors' => $partData['errors'] ?? []
|
|
||||||
];
|
|
||||||
|
|
||||||
foreach ($partData['search_results'] as $resultData) {
|
|
||||||
$dtoData = $resultData['dto'];
|
|
||||||
|
|
||||||
$dto = new SearchResultDTO(
|
|
||||||
provider_key: $dtoData['provider_key'],
|
|
||||||
provider_id: $dtoData['provider_id'],
|
|
||||||
name: $dtoData['name'],
|
|
||||||
description: $dtoData['description'],
|
|
||||||
manufacturer: $dtoData['manufacturer'],
|
|
||||||
mpn: $dtoData['mpn'],
|
|
||||||
provider_url: $dtoData['provider_url'],
|
|
||||||
preview_image_url: $dtoData['preview_image_url']
|
|
||||||
);
|
|
||||||
|
|
||||||
$localPart = null;
|
|
||||||
if ($resultData['localPart'] && $entityManager) {
|
|
||||||
$localPart = $entityManager->getRepository(Part::class)->find($resultData['localPart']);
|
|
||||||
}
|
|
||||||
|
|
||||||
$partResult['search_results'][] = [
|
|
||||||
'dto' => $dto,
|
|
||||||
'localPart' => $localPart,
|
|
||||||
'source_field' => $dtoData['_source_field'] ?? null,
|
|
||||||
'source_keyword' => $dtoData['_source_keyword'] ?? null
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
$searchResults[] = $partResult;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $searchResults;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,14 +4,12 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem;
|
namespace App\Services\InfoProviderSystem;
|
||||||
|
|
||||||
use App\Entity\Base\AbstractPartsContainingDBElement;
|
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
use App\Entity\Parts\Supplier;
|
use App\Entity\Parts\Supplier;
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchResultDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResultDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\PartSearchResultDTO;
|
use App\Services\InfoProviderSystem\DTOs\PartSearchResultsDTO;
|
||||||
use App\Services\InfoProviderSystem\DTOs\SearchResultWithMetadataDTO;
|
|
||||||
use App\Services\InfoProviderSystem\Providers\BatchInfoProviderInterface;
|
use App\Services\InfoProviderSystem\Providers\BatchInfoProviderInterface;
|
||||||
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
use App\Services\InfoProviderSystem\Providers\InfoProviderInterface;
|
||||||
use Doctrine\ORM\EntityManagerInterface;
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
@ -94,7 +92,7 @@ final class BulkInfoProviderService
|
||||||
$searchResults = $this->formatSearchResults($allResults);
|
$searchResults = $this->formatSearchResults($allResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
$partResults[] = new PartSearchResultDTO(
|
$partResults[] = new PartSearchResultsDTO(
|
||||||
part: $part,
|
part: $part,
|
||||||
searchResults: $searchResults,
|
searchResults: $searchResults,
|
||||||
errors: []
|
errors: []
|
||||||
|
|
@ -148,7 +146,7 @@ final class BulkInfoProviderService
|
||||||
if ($keyword && isset($providerResults[$keyword])) {
|
if ($keyword && isset($providerResults[$keyword])) {
|
||||||
foreach ($providerResults[$keyword] as $dto) {
|
foreach ($providerResults[$keyword] as $dto) {
|
||||||
$batchResults[$part->getId()][] = new BulkSearchResultDTO(
|
$batchResults[$part->getId()][] = new BulkSearchResultDTO(
|
||||||
baseDto: $dto,
|
searchResult: $dto,
|
||||||
sourceField: $mapping->field,
|
sourceField: $mapping->field,
|
||||||
sourceKeyword: $keyword,
|
sourceKeyword: $keyword,
|
||||||
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
||||||
|
|
@ -207,7 +205,7 @@ final class BulkInfoProviderService
|
||||||
|
|
||||||
foreach ($dtos as $dto) {
|
foreach ($dtos as $dto) {
|
||||||
$regularResults[$part->getId()][] = new BulkSearchResultDTO(
|
$regularResults[$part->getId()][] = new BulkSearchResultDTO(
|
||||||
baseDto: $dto,
|
searchResult: $dto,
|
||||||
sourceField: $mapping->field,
|
sourceField: $mapping->field,
|
||||||
sourceKeyword: $keyword,
|
sourceKeyword: $keyword,
|
||||||
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
||||||
|
|
@ -329,7 +327,7 @@ final class BulkInfoProviderService
|
||||||
* Format and deduplicate search results.
|
* Format and deduplicate search results.
|
||||||
*
|
*
|
||||||
* @param BulkSearchResultDTO[] $bulkResults Array of bulk search results
|
* @param BulkSearchResultDTO[] $bulkResults Array of bulk search results
|
||||||
* @return SearchResultWithMetadataDTO[] Array of formatted search results with metadata
|
* @return BulkSearchResultDTO[] Array of formatted search results with metadata
|
||||||
*/
|
*/
|
||||||
private function formatSearchResults(array $bulkResults): array
|
private function formatSearchResults(array $bulkResults): array
|
||||||
{
|
{
|
||||||
|
|
@ -340,15 +338,10 @@ final class BulkInfoProviderService
|
||||||
$seenKeys = [];
|
$seenKeys = [];
|
||||||
|
|
||||||
foreach ($bulkResults as $result) {
|
foreach ($bulkResults as $result) {
|
||||||
$key = "{$result->getProviderKey()}|{$result->getProviderId()}";
|
$key = "{$result->searchResult->provider_key}|{$result->searchResult->provider_id}";
|
||||||
if (!in_array($key, $seenKeys, true)) {
|
if (!in_array($key, $seenKeys, true)) {
|
||||||
$seenKeys[] = $key;
|
$seenKeys[] = $key;
|
||||||
$uniqueResults[] = new SearchResultWithMetadataDTO(
|
$uniqueResults[] = $result;
|
||||||
searchResult: $result,
|
|
||||||
localPart: $result->localPart,
|
|
||||||
sourceField: $result->sourceField,
|
|
||||||
sourceKeyword: $result->sourceKeyword
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -358,49 +351,29 @@ final class BulkInfoProviderService
|
||||||
/**
|
/**
|
||||||
* Prefetch detailed information for search results.
|
* Prefetch detailed information for search results.
|
||||||
*
|
*
|
||||||
* @param BulkSearchResponseDTO|array $searchResults Search results (supports both new DTO and legacy array format)
|
* @param BulkSearchResponseDTO $searchResults Search results (supports both new DTO and legacy array format)
|
||||||
*/
|
*/
|
||||||
public function prefetchDetailsForResults($searchResults): void
|
public function prefetchDetailsForResults(BulkSearchResponseDTO $searchResults): void
|
||||||
{
|
{
|
||||||
$prefetchCount = 0;
|
$prefetchCount = 0;
|
||||||
|
|
||||||
// Handle both new DTO format and legacy array format for backwards compatibility
|
// Handle both new DTO format and legacy array format for backwards compatibility
|
||||||
if ($searchResults instanceof BulkSearchResponseDTO) {
|
|
||||||
foreach ($searchResults->partResults as $partResult) {
|
foreach ($searchResults->partResults as $partResult) {
|
||||||
foreach ($partResult->searchResults as $result) {
|
foreach ($partResult->searchResults as $result) {
|
||||||
$dto = $result->searchResult;
|
$dto = $result->searchResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->infoRetriever->getDetails($dto->getProviderKey(), $dto->getProviderId());
|
$this->infoRetriever->getDetails($dto->provider_key, $dto->provider_id);
|
||||||
$prefetchCount++;
|
$prefetchCount++;
|
||||||
} catch (\Exception $e) {
|
} catch (\Exception $e) {
|
||||||
$this->logger->warning('Failed to prefetch details for provider part', [
|
$this->logger->warning('Failed to prefetch details for provider part', [
|
||||||
'provider_key' => $dto->getProviderKey(),
|
'provider_key' => $dto->provider_key,
|
||||||
'provider_id' => $dto->getProviderId(),
|
'provider_id' => $dto->provider_id,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Legacy array format support
|
|
||||||
foreach ($searchResults as $partResult) {
|
|
||||||
foreach ($partResult['search_results'] as $result) {
|
|
||||||
$dto = $result['dto'];
|
|
||||||
|
|
||||||
try {
|
|
||||||
$this->infoRetriever->getDetails($dto->getProviderKey(), $dto->getProviderId());
|
|
||||||
$prefetchCount++;
|
|
||||||
} catch (\Exception $e) {
|
|
||||||
$this->logger->warning('Failed to prefetch details for provider part', [
|
|
||||||
'provider_key' => $dto->getProviderKey(),
|
|
||||||
'provider_id' => $dto->getProviderId(),
|
|
||||||
'error' => $e->getMessage()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->logger->info("Prefetched details for {$prefetchCount} search results");
|
$this->logger->info("Prefetched details for {$prefetchCount} search results");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,43 +22,40 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem\DTOs;
|
namespace App\Services\InfoProviderSystem\DTOs;
|
||||||
|
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents the complete response from a bulk info provider search operation.
|
* Represents the complete response from a bulk info provider search operation.
|
||||||
* This DTO provides type safety and clear structure instead of complex arrays.
|
* It contains a list of PartSearchResultDTOs, one for each part searched.
|
||||||
*/
|
*/
|
||||||
readonly class BulkSearchResponseDTO
|
readonly class BulkSearchResponseDTO implements \ArrayAccess
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param PartSearchResultDTO[] $partResults Array of search results for each part
|
* @param PartSearchResultsDTO[] $partResults Array of search results for each part
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
public array $partResults
|
public array $partResults
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create from legacy array format for backwards compatibility.
|
* Replaces the search results for a specific part, and returns a new instance.
|
||||||
* @param array $data Array of part result arrays in legacy format
|
* @param Part|int $part
|
||||||
|
* @param PartSearchResultsDTO $new_results
|
||||||
|
* @return BulkSearchResponseDTO
|
||||||
*/
|
*/
|
||||||
public static function fromArray(array $data): self
|
public function replaceResultsForPart(Part|int $part, PartSearchResultsDTO $new_results): self
|
||||||
{
|
{
|
||||||
$partResults = [];
|
$array = $this->partResults;
|
||||||
foreach ($data as $partData) {
|
foreach ($array as $index => $partResult) {
|
||||||
$partResults[] = PartSearchResultDTO::fromArray($partData);
|
if (($part instanceof Part && $partResult->part->getId() === $part->getId()) ||
|
||||||
|
($partResult->part->getId() === $part)) {
|
||||||
|
$array[$index] = $new_results;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new self($partResults);
|
return new self($array);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to legacy array format for backwards compatibility.
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
$result = [];
|
|
||||||
foreach ($this->partResults as $partResult) {
|
|
||||||
$result[] = $partResult->toArray();
|
|
||||||
}
|
|
||||||
return $result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,7 +85,7 @@ readonly class BulkSearchResponseDTO
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all parts that have search results.
|
* Get all parts that have search results.
|
||||||
* @return PartSearchResultDTO[]
|
* @return PartSearchResultsDTO[]
|
||||||
*/
|
*/
|
||||||
public function getPartsWithResults(): array
|
public function getPartsWithResults(): array
|
||||||
{
|
{
|
||||||
|
|
@ -97,7 +94,7 @@ readonly class BulkSearchResponseDTO
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all parts that have errors.
|
* Get all parts that have errors.
|
||||||
* @return PartSearchResultDTO[]
|
* @return PartSearchResultsDTO[]
|
||||||
*/
|
*/
|
||||||
public function getPartsWithErrors(): array
|
public function getPartsWithErrors(): array
|
||||||
{
|
{
|
||||||
|
|
@ -119,4 +116,104 @@ readonly class BulkSearchResponseDTO
|
||||||
{
|
{
|
||||||
return count($this->getPartsWithResults());
|
return count($this->getPartsWithResults());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge multiple BulkSearchResponseDTO instances into one.
|
||||||
|
* @param BulkSearchResponseDTO ...$responses
|
||||||
|
* @return BulkSearchResponseDTO
|
||||||
|
*/
|
||||||
|
public static function merge(BulkSearchResponseDTO ...$responses): BulkSearchResponseDTO
|
||||||
|
{
|
||||||
|
$mergedResults = [];
|
||||||
|
foreach ($responses as $response) {
|
||||||
|
foreach ($response->partResults as $partResult) {
|
||||||
|
$mergedResults[] = $partResult;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new BulkSearchResponseDTO($mergedResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this DTO to a serializable representation suitable for storage in the database
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toSerializableRepresentation(): array
|
||||||
|
{
|
||||||
|
$serialized = [];
|
||||||
|
|
||||||
|
foreach ($this->partResults as $partResult) {
|
||||||
|
$partData = [
|
||||||
|
'part_id' => $partResult->part->getId(),
|
||||||
|
'search_results' => [],
|
||||||
|
'errors' => $partResult->errors ?? []
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($partResult->searchResults as $result) {
|
||||||
|
$partData['search_results'][] = [
|
||||||
|
'dto' => $result->searchResult->toNormalizedSearchResultArray(),
|
||||||
|
'source_field' => $result->sourceField ?? null,
|
||||||
|
'source_keyword' => $result->sourceKeyword ?? null,
|
||||||
|
'localPart' => $result->localPart?->getId(),
|
||||||
|
'priority' => $result->priority
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$serialized[] = $partData;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $serialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a BulkSearchResponseDTO from a serializable representation.
|
||||||
|
* @param array $data
|
||||||
|
* @param EntityManagerInterface $entityManager
|
||||||
|
* @return BulkSearchResponseDTO
|
||||||
|
* @throws \Doctrine\ORM\Exception\ORMException
|
||||||
|
*/
|
||||||
|
public static function fromSerializableRepresentation(array $data, EntityManagerInterface $entityManager): BulkSearchResponseDTO
|
||||||
|
{
|
||||||
|
$partResults = [];
|
||||||
|
foreach ($data as $partData) {
|
||||||
|
$partResults[] = new PartSearchResultsDTO(
|
||||||
|
part: $entityManager->getReference(Part::class, $partData['part_id']),
|
||||||
|
searchResults: array_map(fn($result) => new BulkSearchResultDTO(
|
||||||
|
searchResult: SearchResultDTO::fromNormalizedSearchResultArray($result['dto']),
|
||||||
|
sourceField: $result['source_field'] ?? null,
|
||||||
|
sourceKeyword: $result['source_keyword'] ?? null,
|
||||||
|
localPart: isset($result['localPart']) ? $entityManager->getReference(Part::class, $result['localPart']) : null,
|
||||||
|
priority: $result['priority'] ?? null
|
||||||
|
), $partData['search_results'] ?? []),
|
||||||
|
errors: $partData['errors'] ?? []
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new BulkSearchResponseDTO($partResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetExists(mixed $offset): bool
|
||||||
|
{
|
||||||
|
if (!is_int($offset)) {
|
||||||
|
throw new \InvalidArgumentException("Offset must be an integer.");
|
||||||
|
}
|
||||||
|
return isset($this->partResults[$offset]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetGet(mixed $offset): ?PartSearchResultsDTO
|
||||||
|
{
|
||||||
|
if (!is_int($offset)) {
|
||||||
|
throw new \InvalidArgumentException("Offset must be an integer.");
|
||||||
|
}
|
||||||
|
return $this->partResults[$offset] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetSet(mixed $offset, mixed $value): void
|
||||||
|
{
|
||||||
|
throw new \LogicException("BulkSearchResponseDTO is immutable.");
|
||||||
|
}
|
||||||
|
|
||||||
|
public function offsetUnset(mixed $offset): void
|
||||||
|
{
|
||||||
|
throw new \LogicException('BulkSearchResponseDTO is immutable.');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -22,18 +22,16 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem\DTOs;
|
namespace App\Services\InfoProviderSystem\DTOs;
|
||||||
|
|
||||||
use App\Entity\Parts\ManufacturingStatus;
|
|
||||||
use App\Entity\Parts\Part;
|
use App\Entity\Parts\Part;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a search result from bulk search with additional context information.
|
* Represents a search result from bulk search with additional context information, like how the part was found.
|
||||||
* Uses composition instead of inheritance for better maintainability.
|
|
||||||
*/
|
*/
|
||||||
readonly class BulkSearchResultDTO
|
readonly class BulkSearchResultDTO
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
/** The base search result DTO containing provider data */
|
/** The base search result DTO containing provider data */
|
||||||
public SearchResultDTO $baseDto,
|
public SearchResultDTO $searchResult,
|
||||||
/** The field that was used to find this result */
|
/** The field that was used to find this result */
|
||||||
public ?string $sourceField = null,
|
public ?string $sourceField = null,
|
||||||
/** The actual keyword that was searched for */
|
/** The actual keyword that was searched for */
|
||||||
|
|
@ -43,97 +41,4 @@ readonly class BulkSearchResultDTO
|
||||||
/** Priority for this search result */
|
/** Priority for this search result */
|
||||||
public int $priority = 1
|
public int $priority = 1
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
// Delegation methods for SearchResultDTO properties
|
|
||||||
public function getProviderKey(): string
|
|
||||||
{
|
|
||||||
return $this->baseDto->provider_key;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProviderId(): string
|
|
||||||
{
|
|
||||||
return $this->baseDto->provider_id;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getName(): string
|
|
||||||
{
|
|
||||||
return $this->baseDto->name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getDescription(): string
|
|
||||||
{
|
|
||||||
return $this->baseDto->description;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getCategory(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->category;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getManufacturer(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->manufacturer;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getMpn(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->mpn;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPreviewImageUrl(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->preview_image_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getPreviewImageFile(): ?FileDTO
|
|
||||||
{
|
|
||||||
return $this->baseDto->preview_image_file;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getManufacturingStatus(): ?ManufacturingStatus
|
|
||||||
{
|
|
||||||
return $this->baseDto->manufacturing_status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getProviderUrl(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->provider_url;
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFootprint(): ?string
|
|
||||||
{
|
|
||||||
return $this->baseDto->footprint;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backwards compatibility properties for legacy code
|
|
||||||
public function __get(string $name): mixed
|
|
||||||
{
|
|
||||||
return match ($name) {
|
|
||||||
'provider_key' => $this->baseDto->provider_key,
|
|
||||||
'provider_id' => $this->baseDto->provider_id,
|
|
||||||
'name' => $this->baseDto->name,
|
|
||||||
'description' => $this->baseDto->description,
|
|
||||||
'category' => $this->baseDto->category,
|
|
||||||
'manufacturer' => $this->baseDto->manufacturer,
|
|
||||||
'mpn' => $this->baseDto->mpn,
|
|
||||||
'preview_image_url' => $this->baseDto->preview_image_url,
|
|
||||||
'preview_image_file' => $this->baseDto->preview_image_file,
|
|
||||||
'manufacturing_status' => $this->baseDto->manufacturing_status,
|
|
||||||
'provider_url' => $this->baseDto->provider_url,
|
|
||||||
'footprint' => $this->baseDto->footprint,
|
|
||||||
default => throw new \InvalidArgumentException("Property '{$name}' does not exist")
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Magic isset method for backwards compatibility.
|
|
||||||
*/
|
|
||||||
public function __isset(string $name): bool
|
|
||||||
{
|
|
||||||
return in_array($name, [
|
|
||||||
'provider_key', 'provider_id', 'name', 'description', 'category',
|
|
||||||
'manufacturer', 'mpn', 'preview_image_url', 'preview_image_file',
|
|
||||||
'manufacturing_status', 'provider_url', 'footprint'
|
|
||||||
], true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
@ -28,11 +28,11 @@ use App\Entity\Parts\Part;
|
||||||
* Represents the search results for a single part from bulk info provider search.
|
* Represents the search results for a single part from bulk info provider search.
|
||||||
* This DTO provides type safety and clear structure for part search results.
|
* This DTO provides type safety and clear structure for part search results.
|
||||||
*/
|
*/
|
||||||
readonly class PartSearchResultDTO
|
readonly class PartSearchResultsDTO
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* @param Part $part The part that was searched for
|
* @param Part $part The part that was searched for
|
||||||
* @param SearchResultWithMetadataDTO[] $searchResults Array of search results found for this part
|
* @param BulkSearchResultDTO[] $searchResults Array of search results found for this part
|
||||||
* @param string[] $errors Array of error messages encountered during search
|
* @param string[] $errors Array of error messages encountered during search
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
|
|
@ -41,42 +41,6 @@ readonly class PartSearchResultDTO
|
||||||
public array $errors = []
|
public array $errors = []
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create from legacy array format for backwards compatibility.
|
|
||||||
* @param array{part: Part, search_results: array, errors: string[]} $data
|
|
||||||
*/
|
|
||||||
public static function fromArray(array $data): self
|
|
||||||
{
|
|
||||||
$searchResults = [];
|
|
||||||
foreach ($data['search_results'] as $result) {
|
|
||||||
$searchResults[] = SearchResultWithMetadataDTO::fromArray($result);
|
|
||||||
}
|
|
||||||
|
|
||||||
return new self(
|
|
||||||
part: $data['part'],
|
|
||||||
searchResults: $searchResults,
|
|
||||||
errors: $data['errors'] ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to legacy array format for backwards compatibility.
|
|
||||||
* @return array{part: Part, search_results: array, errors: string[]}
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
$searchResults = [];
|
|
||||||
foreach ($this->searchResults as $result) {
|
|
||||||
$searchResults[] = $result->toArray();
|
|
||||||
}
|
|
||||||
|
|
||||||
return [
|
|
||||||
'part' => $this->part,
|
|
||||||
'search_results' => $searchResults,
|
|
||||||
'errors' => $this->errors,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if this part has any search results.
|
* Check if this part has any search results.
|
||||||
*/
|
*/
|
||||||
|
|
@ -101,14 +65,19 @@ readonly class PartSearchResultDTO
|
||||||
return count($this->searchResults);
|
return count($this->searchResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getErrorCount(): int
|
||||||
|
{
|
||||||
|
return count($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get search results sorted by priority (ascending).
|
* Get search results sorted by priority (ascending).
|
||||||
* @return SearchResultWithMetadataDTO[]
|
* @return BulkSearchResultDTO[]
|
||||||
*/
|
*/
|
||||||
public function getResultsSortedByPriority(): array
|
public function getResultsSortedByPriority(): array
|
||||||
{
|
{
|
||||||
$results = $this->searchResults;
|
$results = $this->searchResults;
|
||||||
usort($results, fn($a, $b) => $a->getPriority() <=> $b->getPriority());
|
usort($results, static fn(BulkSearchResultDTO $a, BulkSearchResultDTO $b) => $a->priority <=> $b->priority);
|
||||||
return $results;
|
return $results;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -59,8 +59,8 @@ class SearchResultDTO
|
||||||
public readonly ?string $provider_url = null,
|
public readonly ?string $provider_url = null,
|
||||||
/** @var string|null A footprint representation of the providers page */
|
/** @var string|null A footprint representation of the providers page */
|
||||||
public readonly ?string $footprint = null,
|
public readonly ?string $footprint = null,
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
if ($preview_image_url !== null) {
|
if ($preview_image_url !== null) {
|
||||||
//Utilize the escaping mechanism of FileDTO to ensure that the preview image URL is correctly encoded
|
//Utilize the escaping mechanism of FileDTO to ensure that the preview image URL is correctly encoded
|
||||||
//See issue #521: https://github.com/Part-DB/Part-DB-server/issues/521
|
//See issue #521: https://github.com/Part-DB/Part-DB-server/issues/521
|
||||||
|
|
@ -71,4 +71,47 @@ class SearchResultDTO
|
||||||
$this->preview_image_url = null;
|
$this->preview_image_url = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method creates a normalized array representation of the DTO.
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function toNormalizedSearchResultArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'provider_key' => $this->provider_key,
|
||||||
|
'provider_id' => $this->provider_id,
|
||||||
|
'name' => $this->name,
|
||||||
|
'description' => $this->description,
|
||||||
|
'category' => $this->category,
|
||||||
|
'manufacturer' => $this->manufacturer,
|
||||||
|
'mpn' => $this->mpn,
|
||||||
|
'preview_image_url' => $this->preview_image_url,
|
||||||
|
'manufacturing_status' => $this->manufacturing_status?->value,
|
||||||
|
'provider_url' => $this->provider_url,
|
||||||
|
'footprint' => $this->footprint,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a SearchResultDTO from a normalized array representation.
|
||||||
|
* @param array $data
|
||||||
|
* @return self
|
||||||
|
*/
|
||||||
|
public static function fromNormalizedSearchResultArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
provider_key: $data['provider_key'],
|
||||||
|
provider_id: $data['provider_id'],
|
||||||
|
name: $data['name'],
|
||||||
|
description: $data['description'],
|
||||||
|
category: $data['category'] ?? null,
|
||||||
|
manufacturer: $data['manufacturer'] ?? null,
|
||||||
|
mpn: $data['mpn'] ?? null,
|
||||||
|
preview_image_url: $data['preview_image_url'] ?? null,
|
||||||
|
manufacturing_status: isset($data['manufacturing_status']) ? ManufacturingStatus::tryFrom($data['manufacturing_status']) : null,
|
||||||
|
provider_url: $data['provider_url'] ?? null,
|
||||||
|
footprint: $data['footprint'] ?? null,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
<?php
|
|
||||||
/*
|
|
||||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
|
||||||
*
|
|
||||||
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU Affero General Public License as published
|
|
||||||
* by the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU Affero General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU Affero General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem\DTOs;
|
|
||||||
|
|
||||||
use App\Entity\Parts\Part;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Represents a search result with additional metadata about how it was found.
|
|
||||||
* This DTO encapsulates both the search result data and the context of the search.
|
|
||||||
*/
|
|
||||||
readonly class SearchResultWithMetadataDTO
|
|
||||||
{
|
|
||||||
public function __construct(
|
|
||||||
/** The search result DTO containing part information from the provider */
|
|
||||||
public BulkSearchResultDTO $searchResult,
|
|
||||||
/** Local part that matches this search result, if any */
|
|
||||||
public ?Part $localPart = null,
|
|
||||||
/** The field that was used to find this result (e.g., 'mpn', 'name') */
|
|
||||||
public ?string $sourceField = null,
|
|
||||||
/** The actual keyword/value that was searched for */
|
|
||||||
public ?string $sourceKeyword = null
|
|
||||||
) {}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create from legacy array format for backwards compatibility.
|
|
||||||
* @param array{dto: BulkSearchResultDTO, localPart: ?Part, source_field: string, source_keyword: string} $data
|
|
||||||
*/
|
|
||||||
public static function fromArray(array $data): self
|
|
||||||
{
|
|
||||||
return new self(
|
|
||||||
searchResult: $data['dto'],
|
|
||||||
localPart: $data['localPart'] ?? null,
|
|
||||||
sourceField: $data['source_field'] ?? null,
|
|
||||||
sourceKeyword: $data['source_keyword'] ?? null
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert to legacy array format for backwards compatibility.
|
|
||||||
* @return array{dto: BulkSearchResultDTO, localPart: ?Part, source_field: ?string, source_keyword: ?string}
|
|
||||||
*/
|
|
||||||
public function toArray(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
'dto' => $this->searchResult,
|
|
||||||
'localPart' => $this->localPart,
|
|
||||||
'source_field' => $this->sourceField,
|
|
||||||
'source_keyword' => $this->sourceKeyword,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the priority of this search result.
|
|
||||||
*/
|
|
||||||
public function getPriority(): int
|
|
||||||
{
|
|
||||||
return $this->searchResult->priority;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the provider key from the search result.
|
|
||||||
*/
|
|
||||||
public function getProviderKey(): string
|
|
||||||
{
|
|
||||||
return $this->searchResult->getProviderKey();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the provider ID from the search result.
|
|
||||||
*/
|
|
||||||
public function getProviderId(): string
|
|
||||||
{
|
|
||||||
return $this->searchResult->getProviderId();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?php
|
||||||
|
/*
|
||||||
|
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||||
|
*
|
||||||
|
* Copyright (C) 2019 - 2025 Jan Böhmer (https://github.com/jbtronics)
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published
|
||||||
|
* by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Services\InfoProviderSystem\DTOs;
|
||||||
|
|
||||||
|
use App\Doctrine\Types\BulkSearchResponseDTOType;
|
||||||
|
use App\Entity\Parts\Part;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResultDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\PartSearchResultsDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
|
||||||
|
class BulkSearchResponseDTOTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
private EntityManagerInterface $entityManager;
|
||||||
|
|
||||||
|
private BulkSearchResponseDTO $dummyEmpty;
|
||||||
|
private BulkSearchResponseDTO $dummy;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
self::bootKernel();
|
||||||
|
$this->entityManager = self::getContainer()->get(EntityManagerInterface::class);
|
||||||
|
|
||||||
|
$this->dummyEmpty = new BulkSearchResponseDTO(partResults: []);
|
||||||
|
$this->dummy = new BulkSearchResponseDTO(partResults: [
|
||||||
|
new PartSearchResultsDTO(
|
||||||
|
part: $this->entityManager->find(Part::class, 1),
|
||||||
|
searchResults: [
|
||||||
|
new BulkSearchResultDTO(
|
||||||
|
searchResult: new SearchResultDTO(provider_key: "dummy", provider_id: "1234", name: "Test Part", description: "A part for testing"),
|
||||||
|
sourceField: "mpn", sourceKeyword: "1234", priority: 1
|
||||||
|
),
|
||||||
|
new BulkSearchResultDTO(
|
||||||
|
searchResult: new SearchResultDTO(provider_key: "test", provider_id: "test", name: "Test Part2", description: "A part for testing"),
|
||||||
|
sourceField: "name", sourceKeyword: "1234",
|
||||||
|
localPart: $this->entityManager->find(Part::class, 2), priority: 2,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
errors: ['Error 1']
|
||||||
|
)
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSerializationBackAndForthEmpty(): void
|
||||||
|
{
|
||||||
|
$serialized = $this->dummyEmpty->toSerializableRepresentation();
|
||||||
|
//Ensure that it is json_encodable
|
||||||
|
$json = json_encode($serialized, JSON_THROW_ON_ERROR);
|
||||||
|
$this->assertJson($json);
|
||||||
|
$deserialized = BulkSearchResponseDTO::fromSerializableRepresentation(json_decode($json), $this->entityManager);
|
||||||
|
|
||||||
|
$this->assertEquals($this->dummyEmpty, $deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testSerializationBackAndForth(): void
|
||||||
|
{
|
||||||
|
$serialized = $this->dummy->toSerializableRepresentation();
|
||||||
|
//Ensure that it is json_encodable
|
||||||
|
$this->assertJson(json_encode($serialized, JSON_THROW_ON_ERROR));
|
||||||
|
$deserialized = BulkSearchResponseDTO::fromSerializableRepresentation($serialized, $this->entityManager);
|
||||||
|
|
||||||
|
$this->assertEquals($this->dummy, $deserialized);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testToSerializableRepresentation(): void
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFromSerializableRepresentation(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue