diff --git a/src/Controller/BulkInfoProviderImportController.php b/src/Controller/BulkInfoProviderImportController.php index 9744cece..7675b010 100644 --- a/src/Controller/BulkInfoProviderImportController.php +++ b/src/Controller/BulkInfoProviderImportController.php @@ -182,7 +182,7 @@ class BulkInfoProviderImportController extends AbstractController $searchRequest = new BulkSearchRequestDTO( fieldMappings: $fieldMappings, prefetchDetails: $prefetchDetails, - partIds: $partIds + parts: $parts ); $searchResults = $this->bulkService->performBulkSearch($searchRequest); @@ -444,7 +444,7 @@ class BulkInfoProviderImportController extends AbstractController $searchRequest = new BulkSearchRequestDTO( fieldMappings: $fieldMappings, prefetchDetails: $prefetchDetails, - partIds: [$partId] + parts: [$part] ); try { @@ -501,14 +501,16 @@ class BulkInfoProviderImportController extends AbstractController } // Get all part IDs that are not completed or skipped + $parts = []; $partIds = []; foreach ($job->getJobParts() as $jobPart) { if (!$jobPart->isCompleted() && !$jobPart->isSkipped()) { - $partIds[] = $jobPart->getPart()->getId(); + $parts[] = $jobPart->getPart(); + $partsIds[] = $jobPart->getPart()->getId(); } } - if (empty($partIds)) { + if (empty($parts)) { return $this->json([ 'success' => true, 'message' => 'No parts to research', @@ -523,13 +525,13 @@ class BulkInfoProviderImportController extends AbstractController // Process in batches to reduce memory usage for large operations $batchSize = 20; // Configurable batch size for memory management $allResults = []; - $batches = array_chunk($partIds, $batchSize); + $batches = array_chunk($parts, $batchSize); foreach ($batches as $batch) { $searchRequest = new BulkSearchRequestDTO( fieldMappings: $fieldMappings, prefetchDetails: $prefetchDetails, - partIds: $batch + parts: $batch ); $batchResults = $this->bulkService->performBulkSearch($searchRequest); @@ -552,8 +554,8 @@ class BulkInfoProviderImportController extends AbstractController return $this->json([ 'success' => true, - 'researched_count' => count($partIds), - 'message' => sprintf('Successfully researched %d parts', count($partIds)) + 'researched_count' => count($parts), + 'message' => sprintf('Successfully researched %d parts', count($parts)) ]); } catch (\Exception $e) { @@ -562,7 +564,7 @@ class BulkInfoProviderImportController extends AbstractController 500, [ 'job_id' => $jobId, - 'part_ids' => $partIds, + 'part_ids' => $partsIds, 'exception' => $e->getMessage() ] ); diff --git a/src/Entity/BulkInfoProviderImportJob.php b/src/Entity/BulkInfoProviderImportJob.php index 724b62f8..bfbfd88c 100644 --- a/src/Entity/BulkInfoProviderImportJob.php +++ b/src/Entity/BulkInfoProviderImportJob.php @@ -397,6 +397,10 @@ class BulkInfoProviderImportJob extends AbstractDBElement 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 = []; @@ -433,6 +437,10 @@ class BulkInfoProviderImportJob extends AbstractDBElement 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)) { diff --git a/src/Services/InfoProviderSystem/BulkInfoProviderService.php b/src/Services/InfoProviderSystem/BulkInfoProviderService.php index b78ca450..39391410 100644 --- a/src/Services/InfoProviderSystem/BulkInfoProviderService.php +++ b/src/Services/InfoProviderSystem/BulkInfoProviderService.php @@ -27,11 +27,7 @@ final class BulkInfoProviderService public function performBulkSearch(BulkSearchRequestDTO $request): array { - // Convert string IDs to integers - $partIds = array_map('intval', $request->partIds); - - $partRepository = $this->entityManager->getRepository(Part::class); - $parts = $partRepository->getElementsFromIDArray($partIds); + $parts = $request->parts; if (empty($parts)) { throw new \InvalidArgumentException('No valid parts found for bulk import'); @@ -43,7 +39,7 @@ final class BulkInfoProviderService // Group providers by batch capability $batchProviders = []; $regularProviders = []; - + foreach ($request->fieldMappings as $mapping) { $providers = $mapping['providers'] ?? []; foreach ($providers as $providerKey) { @@ -54,7 +50,7 @@ final class BulkInfoProviderService ]); continue; } - + $provider = $this->providerRegistry->getProviderByKey($providerKey); if ($provider instanceof BatchInfoProviderInterface) { $batchProviders[$providerKey] = $provider; @@ -66,7 +62,7 @@ final class BulkInfoProviderService // Process batch providers first (more efficient) $batchResults = $this->processBatchProviders($parts, $request->fieldMappings, $batchProviders); - + // Process regular providers $regularResults = $this->processRegularProviders($parts, $request->fieldMappings, $regularProviders, $batchResults); @@ -102,24 +98,24 @@ final class BulkInfoProviderService private function processBatchProviders(array $parts, array $fieldMappings, array $batchProviders): array { $batchResults = []; - + foreach ($batchProviders as $providerKey => $provider) { $keywords = $this->collectKeywordsForProvider($parts, $fieldMappings, $providerKey); - + if (empty($keywords)) { continue; } try { $providerResults = $provider->searchByKeywordsBatch($keywords); - + // Map results back to parts foreach ($parts as $part) { foreach ($fieldMappings as $mapping) { if (!in_array($providerKey, $mapping['providers'] ?? [], true)) { continue; } - + $keyword = $this->getKeywordFromField($part, $mapping['field']); if ($keyword && isset($providerResults[$keyword])) { foreach ($providerResults[$keyword] as $dto) { @@ -145,13 +141,20 @@ final class BulkInfoProviderService return $batchResults; } + /** + * @param Part[] $parts + * @param array $fieldMappings + * @param array $regularProviders + * @param array $excludeResults + * @return array + */ private function processRegularProviders(array $parts, array $fieldMappings, array $regularProviders, array $excludeResults): array { $regularResults = []; - + foreach ($parts as $part) { $regularResults[$part->getId()] = []; - + // Skip if we already have batch results for this part if (!empty($excludeResults[$part->getId()] ?? [])) { continue; @@ -160,7 +163,7 @@ final class BulkInfoProviderService foreach ($fieldMappings as $mapping) { $field = $mapping['field']; $providers = array_intersect($mapping['providers'] ?? [], array_keys($regularProviders)); - + if (empty($providers)) { continue; } @@ -172,7 +175,7 @@ final class BulkInfoProviderService try { $dtos = $this->infoRetriever->searchByKeyword($keyword, $providers); - + foreach ($dtos as $dto) { $regularResults[$part->getId()][] = new BulkSearchResultDTO( baseDto: $dto, @@ -198,13 +201,13 @@ final class BulkInfoProviderService private function collectKeywordsForProvider(array $parts, array $fieldMappings, string $providerKey): array { $keywords = []; - + foreach ($parts as $part) { foreach ($fieldMappings as $mapping) { if (!in_array($providerKey, $mapping['providers'] ?? [], true)) { continue; } - + $keyword = $this->getKeywordFromField($part, $mapping['field']); if ($keyword && !in_array($keyword, $keywords, true)) { $keywords[] = $keyword; @@ -251,10 +254,10 @@ final class BulkInfoProviderService { // Sort by priority and remove duplicates usort($bulkResults, fn($a, $b) => $a->priority <=> $b->priority); - + $uniqueResults = []; $seenKeys = []; - + foreach ($bulkResults as $result) { $key = "{$result->provider_key}|{$result->provider_id}"; if (!in_array($key, $seenKeys, true)) { @@ -294,4 +297,4 @@ final class BulkInfoProviderService $this->logger->info("Prefetched details for {$prefetchCount} search results"); } -} \ No newline at end of file +} diff --git a/src/Services/InfoProviderSystem/DTOs/BulkSearchRequestDTO.php b/src/Services/InfoProviderSystem/DTOs/BulkSearchRequestDTO.php index 55e1f65a..c915c3be 100644 --- a/src/Services/InfoProviderSystem/DTOs/BulkSearchRequestDTO.php +++ b/src/Services/InfoProviderSystem/DTOs/BulkSearchRequestDTO.php @@ -4,11 +4,18 @@ declare(strict_types=1); namespace App\Services\InfoProviderSystem\DTOs; -class BulkSearchRequestDTO +use App\Entity\Parts\Part; + +readonly class BulkSearchRequestDTO { + /** + * @param array $fieldMappings + * @param bool $prefetchDetails + * @param Part[] $parts The parts for which the bulk search should be performed. + */ public function __construct( - public readonly array $fieldMappings, - public readonly bool $prefetchDetails = false, - public readonly array $partIds = [] + public array $fieldMappings, + public bool $prefetchDetails = false, + public array $parts = [] ) {} -} \ No newline at end of file +} diff --git a/tests/Controller/BulkInfoProviderImportControllerTest.php b/tests/Controller/BulkInfoProviderImportControllerTest.php index 83c9ee78..8dc86e28 100644 --- a/tests/Controller/BulkInfoProviderImportControllerTest.php +++ b/tests/Controller/BulkInfoProviderImportControllerTest.php @@ -664,7 +664,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase ['field' => 'mpn', 'providers' => ['test'], 'priority' => 2] ], prefetchDetails: false, - partIds: [$part->getId()] + parts: [$part->getId()] ); // The service may return an empty result or throw when no results are found @@ -766,7 +766,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $this->assertResponseStatusCodeSame(Response::HTTP_OK); - // Find job from database to avoid detached entity errors + // Find job from database to avoid detached entity errors $jobId = $job->getId(); $entityManager->clear(); $persistedJob = $entityManager->find(BulkInfoProviderImportJob::class, $jobId); @@ -799,7 +799,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase ['field' => 'test_supplier_spn', 'providers' => ['test'], 'priority' => 2] ], prefetchDetails: false, - partIds: [$part->getId()] + parts: [$part->getId()] ); // The service should be able to process the request and throw an exception when no results are found @@ -833,7 +833,7 @@ class BulkInfoProviderImportControllerTest extends WebTestCase ['field' => 'name', 'providers' => ['lcsc'], 'priority' => 1] ], prefetchDetails: false, - partIds: [$part->getId()] + parts: [$part->getId()] ); // The service should be able to process the request and throw an exception when no results are found @@ -962,4 +962,4 @@ class BulkInfoProviderImportControllerTest extends WebTestCase $entityManager->remove($job); $entityManager->flush(); } -} \ No newline at end of file +}