mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 19:19:29 +00:00
Refactor bulk info provider: replace complex arrays with DTOs
- Add BulkSearchResponseDTO, FieldMappingDTO for type safety - Use composition instead of inheritance in BulkSearchResultDTO - Remove unnecessary BulkSearchRequestDTO - Fix N+1 queries and API error handling - Fix Add Mapping button functionality
This commit is contained in:
parent
8998b006e0
commit
2c195d9767
15 changed files with 838 additions and 195 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -48,3 +48,6 @@ yarn-error.log
|
||||||
###> phpstan/phpstan ###
|
###> phpstan/phpstan ###
|
||||||
phpstan.neon
|
phpstan.neon
|
||||||
###< phpstan/phpstan ###
|
###< phpstan/phpstan ###
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
CLAUDE.md
|
||||||
|
|
@ -22,10 +22,8 @@ export default class extends Controller {
|
||||||
select.addEventListener('change', this.updateFieldOptions.bind(this))
|
select.addEventListener('change', this.updateFieldOptions.bind(this))
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add click listener to add button
|
// Note: Add button click is handled by Stimulus action in template (data-action="click->field-mapping#addMapping")
|
||||||
if (this.hasAddButtonTarget) {
|
// No manual event listener needed
|
||||||
this.addButtonTarget.addEventListener('click', this.addMapping.bind(this))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Form submit handler
|
// Form submit handler
|
||||||
const form = this.element.querySelector('form')
|
const form = this.element.querySelector('form')
|
||||||
|
|
|
||||||
|
|
@ -104,3 +104,9 @@ parameters:
|
||||||
env(SAML_ROLE_MAPPING): '{}'
|
env(SAML_ROLE_MAPPING): '{}'
|
||||||
|
|
||||||
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
env(DATABASE_EMULATE_NATURAL_SORT): 0
|
||||||
|
|
||||||
|
######################################################################################################################
|
||||||
|
# Bulk Info Provider Import Configuration
|
||||||
|
######################################################################################################################
|
||||||
|
partdb.bulk_import.batch_size: 20 # Number of parts to process in each batch during bulk operations
|
||||||
|
partdb.bulk_import.max_parts_per_operation: 1000 # Maximum number of parts allowed per bulk import operation
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ services:
|
||||||
bool $gdpr_compliance: '%partdb.gdpr_compliance%'
|
bool $gdpr_compliance: '%partdb.gdpr_compliance%'
|
||||||
bool $kernel_debug_enabled: '%kernel.debug%'
|
bool $kernel_debug_enabled: '%kernel.debug%'
|
||||||
string $kernel_cache_dir: '%kernel.cache_dir%'
|
string $kernel_cache_dir: '%kernel.cache_dir%'
|
||||||
|
int $bulkImportBatchSize: '%partdb.bulk_import.batch_size%'
|
||||||
|
int $bulkImportMaxParts: '%partdb.bulk_import.max_parts_per_operation%'
|
||||||
|
|
||||||
_instanceof:
|
_instanceof:
|
||||||
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
|
App\Services\LabelSystem\PlaceholderProviders\PlaceholderProviderInterface:
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ 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\BulkSearchRequestDTO;
|
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
||||||
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;
|
||||||
|
|
@ -45,10 +45,27 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly BulkInfoProviderService $bulkService,
|
private readonly BulkInfoProviderService $bulkService,
|
||||||
private readonly EntityManagerInterface $entityManager,
|
private readonly EntityManagerInterface $entityManager,
|
||||||
private readonly LoggerInterface $logger
|
private readonly LoggerInterface $logger,
|
||||||
|
private readonly int $bulkImportBatchSize,
|
||||||
|
private readonly int $bulkImportMaxParts
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert field mappings from array format to FieldMappingDTO[].
|
||||||
|
*
|
||||||
|
* @param array $fieldMappings Array of field mapping arrays
|
||||||
|
* @return FieldMappingDTO[] Array of FieldMappingDTO objects
|
||||||
|
*/
|
||||||
|
private function convertFieldMappingsToDto(array $fieldMappings): array
|
||||||
|
{
|
||||||
|
$dtos = [];
|
||||||
|
foreach ($fieldMappings as $mapping) {
|
||||||
|
$dtos[] = FieldMappingDTO::fromArray($mapping);
|
||||||
|
}
|
||||||
|
return $dtos;
|
||||||
|
}
|
||||||
|
|
||||||
private function createErrorResponse(string $message, int $statusCode = 400, array $context = []): JsonResponse
|
private function createErrorResponse(string $message, int $statusCode = 400, array $context = []): JsonResponse
|
||||||
{
|
{
|
||||||
$this->logger->warning('Bulk import operation failed', array_merge([
|
$this->logger->warning('Bulk import operation failed', array_merge([
|
||||||
|
|
@ -122,7 +139,17 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
return $this->redirectToRoute('homepage');
|
return $this->redirectToRoute('homepage');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (count($parts) > 50) {
|
// Validate against configured maximum
|
||||||
|
if (count($parts) > $this->bulkImportMaxParts) {
|
||||||
|
$this->addFlash('error', sprintf(
|
||||||
|
'Too many parts selected (%d). Maximum allowed is %d parts per operation.',
|
||||||
|
count($parts),
|
||||||
|
$this->bulkImportMaxParts
|
||||||
|
));
|
||||||
|
return $this->redirectToRoute('homepage');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count($parts) > ($this->bulkImportMaxParts / 2)) {
|
||||||
$this->addFlash('warning', 'Processing ' . count($parts) . ' parts may take several minutes and could timeout. Consider processing smaller batches.');
|
$this->addFlash('warning', 'Processing ' . count($parts) . ' parts may take several minutes and could timeout. Consider processing smaller batches.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -164,6 +191,13 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
throw new \RuntimeException('User must be authenticated and of type User');
|
throw new \RuntimeException('User must be authenticated and of type User');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate part count against configuration limit
|
||||||
|
if (count($parts) > $this->bulkImportMaxParts) {
|
||||||
|
$this->addFlash('error', "Too many parts selected. Maximum allowed: {$this->bulkImportMaxParts}");
|
||||||
|
$partIds = array_map(fn($part) => $part->getId(), $parts);
|
||||||
|
return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]);
|
||||||
|
}
|
||||||
|
|
||||||
// Create and save the job
|
// Create and save the job
|
||||||
$job = new BulkInfoProviderImportJob();
|
$job = new BulkInfoProviderImportJob();
|
||||||
$job->setFieldMappings($fieldMappings);
|
$job->setFieldMappings($fieldMappings);
|
||||||
|
|
@ -179,13 +213,11 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$searchRequest = new BulkSearchRequestDTO(
|
$fieldMappingDtos = $this->convertFieldMappingsToDto($fieldMappings);
|
||||||
fieldMappings: $fieldMappings,
|
$searchResultsDto = $this->bulkService->performBulkSearch($parts, $fieldMappingDtos, $prefetchDetails);
|
||||||
prefetchDetails: $prefetchDetails,
|
|
||||||
parts: $parts
|
|
||||||
);
|
|
||||||
|
|
||||||
$searchResults = $this->bulkService->performBulkSearch($searchRequest);
|
// 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($job->serializeSearchResults($searchResults));
|
||||||
|
|
@ -210,6 +242,7 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
$this->entityManager->flush();
|
$this->entityManager->flush();
|
||||||
|
|
||||||
$this->addFlash('error', 'Search failed due to an error: ' . $e->getMessage());
|
$this->addFlash('error', 'Search failed due to an error: ' . $e->getMessage());
|
||||||
|
$partIds = array_map(fn($part) => $part->getId(), $parts);
|
||||||
return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]);
|
return $this->redirectToRoute('bulk_info_provider_step1', ['ids' => implode(',', $partIds)]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -441,14 +474,11 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
$fieldMappings = $job->getFieldMappings();
|
$fieldMappings = $job->getFieldMappings();
|
||||||
$prefetchDetails = $job->isPrefetchDetails();
|
$prefetchDetails = $job->isPrefetchDetails();
|
||||||
|
|
||||||
$searchRequest = new BulkSearchRequestDTO(
|
$fieldMappingDtos = $this->convertFieldMappingsToDto($fieldMappings);
|
||||||
fieldMappings: $fieldMappings,
|
|
||||||
prefetchDetails: $prefetchDetails,
|
|
||||||
parts: [$part]
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$searchResults = $this->bulkService->performBulkSearch($searchRequest);
|
$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')) {
|
||||||
|
|
@ -500,13 +530,11 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
|
return $this->createErrorResponse('Job not found or access denied', 404, ['job_id' => $jobId]);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get all part IDs that are not completed or skipped
|
// Get all parts that are not completed or skipped
|
||||||
$parts = [];
|
$parts = [];
|
||||||
$partIds = [];
|
|
||||||
foreach ($job->getJobParts() as $jobPart) {
|
foreach ($job->getJobParts() as $jobPart) {
|
||||||
if (!$jobPart->isCompleted() && !$jobPart->isSkipped()) {
|
if (!$jobPart->isCompleted() && !$jobPart->isSkipped()) {
|
||||||
$parts[] = $jobPart->getPart();
|
$parts[] = $jobPart->getPart();
|
||||||
$partsIds[] = $jobPart->getPart()->getId();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -520,26 +548,22 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$fieldMappings = $job->getFieldMappings();
|
$fieldMappings = $job->getFieldMappings();
|
||||||
|
$fieldMappingDtos = $this->convertFieldMappingsToDto($fieldMappings);
|
||||||
$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
|
||||||
$batchSize = 20; // Configurable batch size for memory management
|
|
||||||
$allResults = [];
|
$allResults = [];
|
||||||
$batches = array_chunk($parts, $batchSize);
|
$batches = array_chunk($parts, $this->bulkImportBatchSize);
|
||||||
|
|
||||||
foreach ($batches as $batch) {
|
foreach ($batches as $batch) {
|
||||||
$searchRequest = new BulkSearchRequestDTO(
|
$batchResultsDto = $this->bulkService->performBulkSearch($batch, $fieldMappingDtos, $prefetchDetails);
|
||||||
fieldMappings: $fieldMappings,
|
$batchResults = $batchResultsDto->toArray();
|
||||||
prefetchDetails: $prefetchDetails,
|
|
||||||
parts: $batch
|
|
||||||
);
|
|
||||||
|
|
||||||
$batchResults = $this->bulkService->performBulkSearch($searchRequest);
|
|
||||||
$allResults = array_merge($allResults, $batchResults);
|
$allResults = array_merge($allResults, $batchResults);
|
||||||
|
|
||||||
// Clear entity manager periodically to prevent memory issues
|
// Properly manage entity manager memory without losing state
|
||||||
|
$jobId = $job->getId();
|
||||||
$this->entityManager->clear();
|
$this->entityManager->clear();
|
||||||
$job = $this->entityManager->find(BulkInfoProviderImportJob::class, $job->getId());
|
$job = $this->entityManager->find(BulkInfoProviderImportJob::class, $jobId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the job's search results
|
// Update the job's search results
|
||||||
|
|
@ -564,7 +588,7 @@ class BulkInfoProviderImportController extends AbstractController
|
||||||
500,
|
500,
|
||||||
[
|
[
|
||||||
'job_id' => $jobId,
|
'job_id' => $jobId,
|
||||||
'part_ids' => $partsIds,
|
'part_count' => count($parts),
|
||||||
'exception' => $e->getMessage()
|
'exception' => $e->getMessage()
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -4,12 +4,14 @@ declare(strict_types=1);
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem;
|
namespace App\Services\InfoProviderSystem;
|
||||||
|
|
||||||
use A\B;
|
use App\Entity\Base\AbstractPartsContainingDBElement;
|
||||||
use App\Entity\BulkInfoProviderImportJob;
|
|
||||||
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\BulkSearchRequestDTO;
|
|
||||||
use App\Services\InfoProviderSystem\DTOs\BulkSearchResultDTO;
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResultDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\FieldMappingDTO;
|
||||||
|
use App\Services\InfoProviderSystem\DTOs\PartSearchResultDTO;
|
||||||
|
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;
|
||||||
|
|
@ -18,6 +20,9 @@ use Symfony\Component\HttpClient\Exception\ClientException;
|
||||||
|
|
||||||
final class BulkInfoProviderService
|
final class BulkInfoProviderService
|
||||||
{
|
{
|
||||||
|
/** @var array<string, Supplier|null> Cache for normalized supplier names */
|
||||||
|
private array $supplierCache = [];
|
||||||
|
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly PartInfoRetriever $infoRetriever,
|
private readonly PartInfoRetriever $infoRetriever,
|
||||||
private readonly ExistingPartFinder $existingPartFinder,
|
private readonly ExistingPartFinder $existingPartFinder,
|
||||||
|
|
@ -26,24 +31,31 @@ final class BulkInfoProviderService
|
||||||
private readonly LoggerInterface $logger
|
private readonly LoggerInterface $logger
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public function performBulkSearch(BulkSearchRequestDTO $request): array
|
/**
|
||||||
|
* Perform bulk search across multiple parts and providers.
|
||||||
|
*
|
||||||
|
* @param Part[] $parts Array of parts to search for
|
||||||
|
* @param FieldMappingDTO[] $fieldMappings Array of field mappings defining search strategy
|
||||||
|
* @param bool $prefetchDetails Whether to prefetch detailed information for results
|
||||||
|
* @return BulkSearchResponseDTO Structured response containing all search results
|
||||||
|
* @throws \InvalidArgumentException If no valid parts provided
|
||||||
|
* @throws \RuntimeException If no search results found for any parts
|
||||||
|
*/
|
||||||
|
public function performBulkSearch(array $parts, array $fieldMappings, bool $prefetchDetails = false): BulkSearchResponseDTO
|
||||||
{
|
{
|
||||||
$parts = $request->parts;
|
|
||||||
|
|
||||||
if (empty($parts)) {
|
if (empty($parts)) {
|
||||||
throw new \InvalidArgumentException('No valid parts found for bulk import');
|
throw new \InvalidArgumentException('No valid parts found for bulk import');
|
||||||
}
|
}
|
||||||
|
|
||||||
$searchResults = [];
|
$partResults = [];
|
||||||
$hasAnyResults = false;
|
$hasAnyResults = false;
|
||||||
|
|
||||||
// Group providers by batch capability
|
// Group providers by batch capability
|
||||||
$batchProviders = [];
|
$batchProviders = [];
|
||||||
$regularProviders = [];
|
$regularProviders = [];
|
||||||
|
|
||||||
foreach ($request->fieldMappings as $mapping) {
|
foreach ($fieldMappings as $mapping) {
|
||||||
$providers = $mapping['providers'] ?? [];
|
foreach ($mapping->providers as $providerKey) {
|
||||||
foreach ($providers as $providerKey) {
|
|
||||||
if (!is_string($providerKey)) {
|
if (!is_string($providerKey)) {
|
||||||
$this->logger->error('Invalid provider key type', [
|
$this->logger->error('Invalid provider key type', [
|
||||||
'providerKey' => $providerKey,
|
'providerKey' => $providerKey,
|
||||||
|
|
@ -62,18 +74,14 @@ final class BulkInfoProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process batch providers first (more efficient)
|
// Process batch providers first (more efficient)
|
||||||
$batchResults = $this->processBatchProviders($parts, $request->fieldMappings, $batchProviders);
|
$batchResults = $this->processBatchProviders($parts, $fieldMappings, $batchProviders);
|
||||||
|
|
||||||
// Process regular providers
|
// Process regular providers
|
||||||
$regularResults = $this->processRegularProviders($parts, $request->fieldMappings, $regularProviders, $batchResults);
|
$regularResults = $this->processRegularProviders($parts, $fieldMappings, $regularProviders, $batchResults);
|
||||||
|
|
||||||
// Combine and format results
|
// Combine and format results for each part
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
$partResult = [
|
$searchResults = [];
|
||||||
'part' => $part,
|
|
||||||
'search_results' => [],
|
|
||||||
'errors' => []
|
|
||||||
];
|
|
||||||
|
|
||||||
// Get results from batch and regular processing
|
// Get results from batch and regular processing
|
||||||
$allResults = array_merge(
|
$allResults = array_merge(
|
||||||
|
|
@ -83,24 +91,37 @@ final class BulkInfoProviderService
|
||||||
|
|
||||||
if (!empty($allResults)) {
|
if (!empty($allResults)) {
|
||||||
$hasAnyResults = true;
|
$hasAnyResults = true;
|
||||||
$partResult['search_results'] = $this->formatSearchResults($allResults);
|
$searchResults = $this->formatSearchResults($allResults);
|
||||||
}
|
}
|
||||||
|
|
||||||
$searchResults[] = $partResult;
|
$partResults[] = new PartSearchResultDTO(
|
||||||
|
part: $part,
|
||||||
|
searchResults: $searchResults,
|
||||||
|
errors: []
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$hasAnyResults) {
|
if (!$hasAnyResults) {
|
||||||
throw new \RuntimeException('No search results found for any of the selected parts');
|
throw new \RuntimeException('No search results found for any of the selected parts');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $searchResults;
|
$response = new BulkSearchResponseDTO($partResults);
|
||||||
|
|
||||||
|
// Prefetch details if requested
|
||||||
|
if ($prefetchDetails) {
|
||||||
|
$this->prefetchDetailsForResults($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Part[] $parts
|
* Process parts using batch-capable info providers.
|
||||||
* @param array $fieldMappings
|
*
|
||||||
* @param array<string, BatchInfoProviderInterface> $batchProviders
|
* @param Part[] $parts Array of parts to search for
|
||||||
* @return array<int, BulkSearchResultDTO[]> A list of results indexed by part ID
|
* @param FieldMappingDTO[] $fieldMappings Array of field mapping configurations
|
||||||
|
* @param array<string, BatchInfoProviderInterface> $batchProviders Batch providers indexed by key
|
||||||
|
* @return array<int, BulkSearchResultDTO[]> Results indexed by part ID
|
||||||
*/
|
*/
|
||||||
private function processBatchProviders(array $parts, array $fieldMappings, array $batchProviders): array
|
private function processBatchProviders(array $parts, array $fieldMappings, array $batchProviders): array
|
||||||
{
|
{
|
||||||
|
|
@ -119,19 +140,19 @@ final class BulkInfoProviderService
|
||||||
// Map results back to parts
|
// Map results back to parts
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
foreach ($fieldMappings as $mapping) {
|
foreach ($fieldMappings as $mapping) {
|
||||||
if (!in_array($providerKey, $mapping['providers'] ?? [], true)) {
|
if (!in_array($providerKey, $mapping->providers, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyword = $this->getKeywordFromField($part, $mapping['field']);
|
$keyword = $this->getKeywordFromField($part, $mapping->field);
|
||||||
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,
|
baseDto: $dto,
|
||||||
sourceField: $mapping['field'],
|
sourceField: $mapping->field,
|
||||||
sourceKeyword: $keyword,
|
sourceKeyword: $keyword,
|
||||||
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
||||||
priority: $mapping['priority'] ?? 1
|
priority: $mapping->priority
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -149,11 +170,13 @@ final class BulkInfoProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Part[] $parts
|
* Process parts using regular (non-batch) info providers.
|
||||||
* @param array $fieldMappings
|
*
|
||||||
* @param array<string, InfoProviderInterface> $regularProviders The info providers that do not support batch searching, indexed by their provider key
|
* @param Part[] $parts Array of parts to search for
|
||||||
* @param array $excludeResults
|
* @param FieldMappingDTO[] $fieldMappings Array of field mapping configurations
|
||||||
* @return array <int, BulkSearchResultDTO[]> A list of results indexed by part ID
|
* @param array<string, InfoProviderInterface> $regularProviders Regular providers indexed by key
|
||||||
|
* @param array<int, BulkSearchResultDTO[]> $excludeResults Results to exclude (from batch processing)
|
||||||
|
* @return array<int, BulkSearchResultDTO[]> Results indexed by part ID
|
||||||
*/
|
*/
|
||||||
private function processRegularProviders(array $parts, array $fieldMappings, array $regularProviders, array $excludeResults): array
|
private function processRegularProviders(array $parts, array $fieldMappings, array $regularProviders, array $excludeResults): array
|
||||||
{
|
{
|
||||||
|
|
@ -168,14 +191,13 @@ final class BulkInfoProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ($fieldMappings as $mapping) {
|
foreach ($fieldMappings as $mapping) {
|
||||||
$field = $mapping['field'];
|
$providers = array_intersect($mapping->providers, array_keys($regularProviders));
|
||||||
$providers = array_intersect($mapping['providers'] ?? [], array_keys($regularProviders));
|
|
||||||
|
|
||||||
if (empty($providers)) {
|
if (empty($providers)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyword = $this->getKeywordFromField($part, $field);
|
$keyword = $this->getKeywordFromField($part, $mapping->field);
|
||||||
if (!$keyword) {
|
if (!$keyword) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -186,16 +208,16 @@ final class BulkInfoProviderService
|
||||||
foreach ($dtos as $dto) {
|
foreach ($dtos as $dto) {
|
||||||
$regularResults[$part->getId()][] = new BulkSearchResultDTO(
|
$regularResults[$part->getId()][] = new BulkSearchResultDTO(
|
||||||
baseDto: $dto,
|
baseDto: $dto,
|
||||||
sourceField: $field,
|
sourceField: $mapping->field,
|
||||||
sourceKeyword: $keyword,
|
sourceKeyword: $keyword,
|
||||||
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
localPart: $this->existingPartFinder->findFirstExisting($dto),
|
||||||
priority: $mapping['priority'] ?? 1
|
priority: $mapping->priority
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (ClientException $e) {
|
} catch (ClientException $e) {
|
||||||
$this->logger->error('Regular search failed', [
|
$this->logger->error('Regular search failed', [
|
||||||
'part_id' => $part->getId(),
|
'part_id' => $part->getId(),
|
||||||
'field' => $field,
|
'field' => $mapping->field,
|
||||||
'error' => $e->getMessage()
|
'error' => $e->getMessage()
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -206,10 +228,12 @@ final class BulkInfoProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Part[] $parts
|
* Collect unique keywords for a specific provider from all parts and field mappings.
|
||||||
* @param array $fieldMappings
|
*
|
||||||
* @param string $providerKey
|
* @param Part[] $parts Array of parts to collect keywords from
|
||||||
* @return string[]
|
* @param FieldMappingDTO[] $fieldMappings Array of field mapping configurations
|
||||||
|
* @param string $providerKey The provider key to collect keywords for
|
||||||
|
* @return string[] Array of unique keywords
|
||||||
*/
|
*/
|
||||||
private function collectKeywordsForProvider(array $parts, array $fieldMappings, string $providerKey): array
|
private function collectKeywordsForProvider(array $parts, array $fieldMappings, string $providerKey): array
|
||||||
{
|
{
|
||||||
|
|
@ -217,11 +241,11 @@ final class BulkInfoProviderService
|
||||||
|
|
||||||
foreach ($parts as $part) {
|
foreach ($parts as $part) {
|
||||||
foreach ($fieldMappings as $mapping) {
|
foreach ($fieldMappings as $mapping) {
|
||||||
if (!in_array($providerKey, $mapping['providers'] ?? [], true)) {
|
if (!in_array($providerKey, $mapping->providers, true)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$keyword = $this->getKeywordFromField($part, $mapping['field']);
|
$keyword = $this->getKeywordFromField($part, $mapping->field);
|
||||||
if ($keyword && !in_array($keyword, $keywords, true)) {
|
if ($keyword && !in_array($keyword, $keywords, true)) {
|
||||||
$keywords[] = $keyword;
|
$keywords[] = $keyword;
|
||||||
}
|
}
|
||||||
|
|
@ -247,22 +271,66 @@ final class BulkInfoProviderService
|
||||||
}
|
}
|
||||||
|
|
||||||
$supplierKey = substr($field, 0, -4);
|
$supplierKey = substr($field, 0, -4);
|
||||||
|
$supplier = $this->getSupplierByNormalizedName($supplierKey);
|
||||||
|
|
||||||
|
if (!$supplier) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$orderDetail = $part->getOrderdetails()->filter(
|
||||||
|
fn($od) => $od->getSupplier()?->getId() === $supplier->getId()
|
||||||
|
)->first();
|
||||||
|
|
||||||
|
return $orderDetail !== false ? $orderDetail->getSupplierpartnr() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get supplier by normalized name with caching to prevent N+1 queries.
|
||||||
|
*
|
||||||
|
* @param string $normalizedKey The normalized supplier key to search for
|
||||||
|
* @return Supplier|null The matching supplier or null if not found
|
||||||
|
*/
|
||||||
|
private function getSupplierByNormalizedName(string $normalizedKey): ?Supplier
|
||||||
|
{
|
||||||
|
// Check cache first
|
||||||
|
if (isset($this->supplierCache[$normalizedKey])) {
|
||||||
|
return $this->supplierCache[$normalizedKey];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use efficient database query with PHP normalization
|
||||||
|
// Since DQL doesn't support REPLACE, we'll load all suppliers once and cache the normalization
|
||||||
|
if (empty($this->supplierCache)) {
|
||||||
|
$this->loadSuppliersIntoCache();
|
||||||
|
}
|
||||||
|
|
||||||
|
$supplier = $this->supplierCache[$normalizedKey] ?? null;
|
||||||
|
|
||||||
|
// Cache the result (including null results to prevent repeated queries)
|
||||||
|
$this->supplierCache[$normalizedKey] = $supplier;
|
||||||
|
|
||||||
|
return $supplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load all suppliers into cache with normalized names to avoid N+1 queries.
|
||||||
|
*/
|
||||||
|
private function loadSuppliersIntoCache(): void
|
||||||
|
{
|
||||||
|
/** @var Supplier[] $suppliers */
|
||||||
$suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
|
$suppliers = $this->entityManager->getRepository(Supplier::class)->findAll();
|
||||||
|
|
||||||
foreach ($suppliers as $supplier) {
|
foreach ($suppliers as $supplier) {
|
||||||
$normalizedName = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
|
$normalizedName = strtolower(str_replace([' ', '-', '_'], '_', $supplier->getName()));
|
||||||
if ($normalizedName === $supplierKey) {
|
$this->supplierCache[$normalizedName] = $supplier;
|
||||||
$orderDetail = $part->getOrderdetails()->filter(
|
|
||||||
fn($od) => $od->getSupplier()?->getId() === $supplier->getId()
|
|
||||||
)->first();
|
|
||||||
|
|
||||||
return $orderDetail ? $orderDetail->getSupplierpartnr() : null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format and deduplicate search results.
|
||||||
|
*
|
||||||
|
* @param BulkSearchResultDTO[] $bulkResults Array of bulk search results
|
||||||
|
* @return SearchResultWithMetadataDTO[] Array of formatted search results with metadata
|
||||||
|
*/
|
||||||
private function formatSearchResults(array $bulkResults): array
|
private function formatSearchResults(array $bulkResults): array
|
||||||
{
|
{
|
||||||
// Sort by priority and remove duplicates
|
// Sort by priority and remove duplicates
|
||||||
|
|
@ -272,38 +340,64 @@ final class BulkInfoProviderService
|
||||||
$seenKeys = [];
|
$seenKeys = [];
|
||||||
|
|
||||||
foreach ($bulkResults as $result) {
|
foreach ($bulkResults as $result) {
|
||||||
$key = "{$result->provider_key}|{$result->provider_id}";
|
$key = "{$result->getProviderKey()}|{$result->getProviderId()}";
|
||||||
if (!in_array($key, $seenKeys, true)) {
|
if (!in_array($key, $seenKeys, true)) {
|
||||||
$seenKeys[] = $key;
|
$seenKeys[] = $key;
|
||||||
$uniqueResults[] = [
|
$uniqueResults[] = new SearchResultWithMetadataDTO(
|
||||||
'dto' => $result,
|
searchResult: $result,
|
||||||
'localPart' => $result->localPart,
|
localPart: $result->localPart,
|
||||||
'source_field' => $result->sourceField,
|
sourceField: $result->sourceField,
|
||||||
'source_keyword' => $result->sourceKeyword
|
sourceKeyword: $result->sourceKeyword
|
||||||
];
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return $uniqueResults;
|
return $uniqueResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prefetchDetailsForResults(array $searchResults): void
|
/**
|
||||||
|
* Prefetch detailed information for search results.
|
||||||
|
*
|
||||||
|
* @param BulkSearchResponseDTO|array $searchResults Search results (supports both new DTO and legacy array format)
|
||||||
|
*/
|
||||||
|
public function prefetchDetailsForResults($searchResults): void
|
||||||
{
|
{
|
||||||
$prefetchCount = 0;
|
$prefetchCount = 0;
|
||||||
|
|
||||||
foreach ($searchResults as $partResult) {
|
// Handle both new DTO format and legacy array format for backwards compatibility
|
||||||
foreach ($partResult['search_results'] as $result) {
|
if ($searchResults instanceof BulkSearchResponseDTO) {
|
||||||
$dto = $result['dto'];
|
foreach ($searchResults->partResults as $partResult) {
|
||||||
|
foreach ($partResult->searchResults as $result) {
|
||||||
|
$dto = $result->searchResult;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->infoRetriever->getDetails($dto->provider_key, $dto->provider_id);
|
$this->infoRetriever->getDetails($dto->getProviderKey(), $dto->getProviderId());
|
||||||
$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->provider_key,
|
'provider_key' => $dto->getProviderKey(),
|
||||||
'provider_id' => $dto->provider_id,
|
'provider_id' => $dto->getProviderId(),
|
||||||
'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()
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Services\InfoProviderSystem\DTOs;
|
|
||||||
|
|
||||||
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 array $fieldMappings,
|
|
||||||
public bool $prefetchDetails = false,
|
|
||||||
public array $parts = []
|
|
||||||
) {}
|
|
||||||
}
|
|
||||||
122
src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php
Normal file
122
src/Services/InfoProviderSystem/DTOs/BulkSearchResponseDTO.php
Normal file
|
|
@ -0,0 +1,122 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the complete response from a bulk info provider search operation.
|
||||||
|
* This DTO provides type safety and clear structure instead of complex arrays.
|
||||||
|
*/
|
||||||
|
readonly class BulkSearchResponseDTO
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param PartSearchResultDTO[] $partResults Array of search results for each part
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public array $partResults
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create from legacy array format for backwards compatibility.
|
||||||
|
* @param array $data Array of part result arrays in legacy format
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
$partResults = [];
|
||||||
|
foreach ($data as $partData) {
|
||||||
|
$partResults[] = PartSearchResultDTO::fromArray($partData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new self($partResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert to legacy array format for backwards compatibility.
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
$result = [];
|
||||||
|
foreach ($this->partResults as $partResult) {
|
||||||
|
$result[] = $partResult->toArray();
|
||||||
|
}
|
||||||
|
return $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if any parts have search results.
|
||||||
|
*/
|
||||||
|
public function hasAnyResults(): bool
|
||||||
|
{
|
||||||
|
foreach ($this->partResults as $partResult) {
|
||||||
|
if ($partResult->hasResults()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total number of search results across all parts.
|
||||||
|
*/
|
||||||
|
public function getTotalResultCount(): int
|
||||||
|
{
|
||||||
|
$count = 0;
|
||||||
|
foreach ($this->partResults as $partResult) {
|
||||||
|
$count += $partResult->getResultCount();
|
||||||
|
}
|
||||||
|
return $count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all parts that have search results.
|
||||||
|
* @return PartSearchResultDTO[]
|
||||||
|
*/
|
||||||
|
public function getPartsWithResults(): array
|
||||||
|
{
|
||||||
|
return array_filter($this->partResults, fn($result) => $result->hasResults());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all parts that have errors.
|
||||||
|
* @return PartSearchResultDTO[]
|
||||||
|
*/
|
||||||
|
public function getPartsWithErrors(): array
|
||||||
|
{
|
||||||
|
return array_filter($this->partResults, fn($result) => $result->hasErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of parts processed.
|
||||||
|
*/
|
||||||
|
public function getPartCount(): int
|
||||||
|
{
|
||||||
|
return count($this->partResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of parts with successful results.
|
||||||
|
*/
|
||||||
|
public function getSuccessfulPartCount(): int
|
||||||
|
{
|
||||||
|
return count($this->getPartsWithResults());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,32 +1,139 @@
|
||||||
<?php
|
<?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);
|
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;
|
||||||
|
|
||||||
class BulkSearchResultDTO extends SearchResultDTO
|
/**
|
||||||
|
* Represents a search result from bulk search with additional context information.
|
||||||
|
* Uses composition instead of inheritance for better maintainability.
|
||||||
|
*/
|
||||||
|
readonly class BulkSearchResultDTO
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
SearchResultDTO $baseDto,
|
/** The base search result DTO containing provider data */
|
||||||
public readonly ?string $sourceField = null,
|
public SearchResultDTO $baseDto,
|
||||||
public readonly ?string $sourceKeyword = null,
|
/** The field that was used to find this result */
|
||||||
public readonly ?Part $localPart = null,
|
public ?string $sourceField = null,
|
||||||
public readonly int $priority = 1
|
/** The actual keyword that was searched for */
|
||||||
) {
|
public ?string $sourceKeyword = null,
|
||||||
parent::__construct(
|
/** Local part that matches this search result, if any */
|
||||||
provider_key: $baseDto->provider_key,
|
public ?Part $localPart = null,
|
||||||
provider_id: $baseDto->provider_id,
|
/** Priority for this search result */
|
||||||
name: $baseDto->name,
|
public int $priority = 1
|
||||||
description: $baseDto->description,
|
) {}
|
||||||
category: $baseDto->category,
|
|
||||||
manufacturer: $baseDto->manufacturer,
|
// Delegation methods for SearchResultDTO properties
|
||||||
mpn: $baseDto->mpn,
|
public function getProviderKey(): string
|
||||||
preview_image_url: $baseDto->preview_image_url,
|
{
|
||||||
manufacturing_status: $baseDto->manufacturing_status,
|
return $this->baseDto->provider_key;
|
||||||
provider_url: $baseDto->provider_url,
|
}
|
||||||
footprint: $baseDto->footprint
|
|
||||||
);
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
92
src/Services/InfoProviderSystem/DTOs/FieldMappingDTO.php
Normal file
92
src/Services/InfoProviderSystem/DTOs/FieldMappingDTO.php
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
<?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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a mapping between a part field and the info providers that should search in that field.
|
||||||
|
* This DTO provides type safety and better structure than raw arrays for field mapping configuration.
|
||||||
|
*/
|
||||||
|
readonly class FieldMappingDTO
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $field The field to search in (e.g., 'mpn', 'name', or supplier-specific fields like 'digikey_spn')
|
||||||
|
* @param string[] $providers Array of provider keys to search with (e.g., ['digikey', 'farnell'])
|
||||||
|
* @param int $priority Priority for this field mapping (1-10, lower numbers = higher priority)
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $field,
|
||||||
|
public array $providers,
|
||||||
|
public int $priority = 1
|
||||||
|
) {
|
||||||
|
if ($priority < 1 || $priority > 10) {
|
||||||
|
throw new \InvalidArgumentException('Priority must be between 1 and 10');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a FieldMappingDTO from legacy array format.
|
||||||
|
* @param array{field: string, providers: string[], priority?: int} $data
|
||||||
|
*/
|
||||||
|
public static function fromArray(array $data): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
field: $data['field'],
|
||||||
|
providers: $data['providers'] ?? [],
|
||||||
|
priority: $data['priority'] ?? 1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert this DTO to the legacy array format for backwards compatibility.
|
||||||
|
* @return array{field: string, providers: string[], priority: int}
|
||||||
|
*/
|
||||||
|
public function toArray(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'field' => $this->field,
|
||||||
|
'providers' => $this->providers,
|
||||||
|
'priority' => $this->priority,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this field mapping is for a supplier part number field.
|
||||||
|
*/
|
||||||
|
public function isSupplierPartNumberField(): bool
|
||||||
|
{
|
||||||
|
return str_ends_with($this->field, '_spn');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the supplier key from a supplier part number field.
|
||||||
|
* Returns null if this is not a supplier part number field.
|
||||||
|
*/
|
||||||
|
public function getSupplierKey(): ?string
|
||||||
|
{
|
||||||
|
if (!$this->isSupplierPartNumberField()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return substr($this->field, 0, -4);
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/Services/InfoProviderSystem/DTOs/PartSearchResultDTO.php
Normal file
114
src/Services/InfoProviderSystem/DTOs/PartSearchResultDTO.php
Normal file
|
|
@ -0,0 +1,114 @@
|
||||||
|
<?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 the search results for a single part from bulk info provider search.
|
||||||
|
* This DTO provides type safety and clear structure for part search results.
|
||||||
|
*/
|
||||||
|
readonly class PartSearchResultDTO
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Part $part The part that was searched for
|
||||||
|
* @param SearchResultWithMetadataDTO[] $searchResults Array of search results found for this part
|
||||||
|
* @param string[] $errors Array of error messages encountered during search
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public Part $part,
|
||||||
|
public array $searchResults = [],
|
||||||
|
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.
|
||||||
|
*/
|
||||||
|
public function hasResults(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if this part has any errors.
|
||||||
|
*/
|
||||||
|
public function hasErrors(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of search results for this part.
|
||||||
|
*/
|
||||||
|
public function getResultCount(): int
|
||||||
|
{
|
||||||
|
return count($this->searchResults);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get search results sorted by priority (ascending).
|
||||||
|
* @return SearchResultWithMetadataDTO[]
|
||||||
|
*/
|
||||||
|
public function getResultsSortedByPriority(): array
|
||||||
|
{
|
||||||
|
$results = $this->searchResults;
|
||||||
|
usort($results, fn($a, $b) => $a->getPriority() <=> $b->getPriority());
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
<?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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -132,6 +132,15 @@ class MouserProvider implements InfoProviderInterface
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Check for API errors before processing response
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
throw new \RuntimeException(sprintf(
|
||||||
|
'Mouser API returned HTTP %d: %s',
|
||||||
|
$response->getStatusCode(),
|
||||||
|
$response->getContent(false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
return $this->responseToDTOArray($response);
|
return $this->responseToDTOArray($response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -169,6 +178,16 @@ class MouserProvider implements InfoProviderInterface
|
||||||
]
|
]
|
||||||
],
|
],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
// Check for API errors before processing response
|
||||||
|
if ($response->getStatusCode() !== 200) {
|
||||||
|
throw new \RuntimeException(sprintf(
|
||||||
|
'Mouser API returned HTTP %d: %s',
|
||||||
|
$response->getStatusCode(),
|
||||||
|
$response->getContent(false)
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
$tmp = $this->responseToDTOArray($response);
|
$tmp = $this->responseToDTOArray($response);
|
||||||
|
|
||||||
//Ensure that we have exactly one result
|
//Ensure that we have exactly one result
|
||||||
|
|
|
||||||
|
|
@ -140,15 +140,14 @@
|
||||||
data-controller="field-mapping"
|
data-controller="field-mapping"
|
||||||
data-field-mapping-mapping-index-value="{{ form.field_mappings|length }}"
|
data-field-mapping-mapping-index-value="{{ form.field_mappings|length }}"
|
||||||
data-field-mapping-max-mappings-value="{{ fieldChoices|length }}"
|
data-field-mapping-max-mappings-value="{{ fieldChoices|length }}"
|
||||||
data-field-mapping-prototype-value="{{ form_widget(form.field_mappings.vars.prototype)|e('js') }}"
|
data-field-mapping-prototype-value="{{ form_widget(form.field_mappings.vars.prototype)|e('html_attr') }}"
|
||||||
data-field-mapping-max-mappings-reached-message-value="{{ 'info_providers.bulk_import.max_mappings_reached'|trans|e('js') }}">
|
data-field-mapping-max-mappings-reached-message-value="{{ 'info_providers.bulk_import.max_mappings_reached'|trans|e('js') }}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">{% trans %}info_providers.bulk_import.field_mappings{% endtrans %}</h5>
|
<h5 class="mb-0">{% trans %}info_providers.bulk_import.field_mappings{% endtrans %}</h5>
|
||||||
<small class="text-muted">{% trans %}info_providers.bulk_import.field_mappings_help{% endtrans %}</small>
|
<small class="text-muted">{% trans %}info_providers.bulk_import.field_mappings_help{% endtrans %}</small>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="table-responsive">
|
<table class="table table-striped">
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{% trans %}info_providers.bulk_search.search_field{% endtrans %}</th>
|
<th>{% trans %}info_providers.bulk_search.search_field{% endtrans %}</th>
|
||||||
|
|
@ -171,11 +170,12 @@
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
<button type="button" class="btn btn-success btn-sm" id="addMappingBtn" data-field-mapping-target="addButton">
|
<button type="button" class="btn btn-success btn-sm" id="addMappingBtn"
|
||||||
<i class="fas fa-plus"></i> {% trans %}info_providers.bulk_import.add_mapping{% endtrans %}
|
data-field-mapping-target="addButton"
|
||||||
</button>
|
data-action="click->field-mapping#addMapping">
|
||||||
</div>
|
<i class="fas fa-plus"></i> {% trans %}info_providers.bulk_import.add_mapping{% endtrans %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -657,20 +657,16 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
// Test that the service can extract keywords from parts
|
// Test that the service can extract keywords from parts
|
||||||
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
||||||
|
|
||||||
// Create a test request to verify the service works
|
// Create field mappings to verify the service works
|
||||||
$request = new \App\Services\InfoProviderSystem\DTOs\BulkSearchRequestDTO(
|
$fieldMappings = [
|
||||||
fieldMappings: [
|
new \App\Services\InfoProviderSystem\DTOs\FieldMappingDTO('name', ['test'], 1),
|
||||||
['field' => 'name', 'providers' => ['test'], 'priority' => 1],
|
new \App\Services\InfoProviderSystem\DTOs\FieldMappingDTO('mpn', ['test'], 2)
|
||||||
['field' => 'mpn', 'providers' => ['test'], 'priority' => 2]
|
];
|
||||||
],
|
|
||||||
prefetchDetails: false,
|
|
||||||
parts: [$part]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The service may return an empty result or throw when no results are found
|
// The service may return an empty result or throw when no results are found
|
||||||
try {
|
try {
|
||||||
$result = $bulkService->performBulkSearch($request);
|
$result = $bulkService->performBulkSearch([$part], $fieldMappings, false);
|
||||||
$this->assertIsArray($result);
|
$this->assertInstanceOf(\App\Services\InfoProviderSystem\DTOs\BulkSearchResponseDTO::class, $result);
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$this->assertStringContainsString('No search results found', $e->getMessage());
|
$this->assertStringContainsString('No search results found', $e->getMessage());
|
||||||
}
|
}
|
||||||
|
|
@ -792,19 +788,15 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
// Test that the service can handle supplier part number fields
|
// Test that the service can handle supplier part number fields
|
||||||
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
||||||
|
|
||||||
// Create a test request with supplier SPN field mapping
|
// Create field mappings with supplier SPN field mapping
|
||||||
$request = new \App\Services\InfoProviderSystem\DTOs\BulkSearchRequestDTO(
|
$fieldMappings = [
|
||||||
fieldMappings: [
|
new \App\Services\InfoProviderSystem\DTOs\FieldMappingDTO('invalid_field', ['test'], 1),
|
||||||
['field' => 'invalid_field', 'providers' => ['test'], 'priority' => 1],
|
new \App\Services\InfoProviderSystem\DTOs\FieldMappingDTO('test_supplier_spn', ['test'], 2)
|
||||||
['field' => 'test_supplier_spn', 'providers' => ['test'], 'priority' => 2]
|
];
|
||||||
],
|
|
||||||
prefetchDetails: false,
|
|
||||||
parts: [$part]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The service should be able to process the request and throw an exception when no results are found
|
// The service should be able to process the request and throw an exception when no results are found
|
||||||
try {
|
try {
|
||||||
$bulkService->performBulkSearch($request);
|
$bulkService->performBulkSearch([$part], $fieldMappings, false);
|
||||||
$this->fail('Expected RuntimeException to be thrown when no search results are found');
|
$this->fail('Expected RuntimeException to be thrown when no search results are found');
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$this->assertStringContainsString('No search results found', $e->getMessage());
|
$this->assertStringContainsString('No search results found', $e->getMessage());
|
||||||
|
|
@ -827,18 +819,14 @@ class BulkInfoProviderImportControllerTest extends WebTestCase
|
||||||
// Test that the service can handle batch processing
|
// Test that the service can handle batch processing
|
||||||
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
$bulkService = $client->getContainer()->get(\App\Services\InfoProviderSystem\BulkInfoProviderService::class);
|
||||||
|
|
||||||
// Create a test request with multiple keywords
|
// Create field mappings with multiple keywords
|
||||||
$request = new \App\Services\InfoProviderSystem\DTOs\BulkSearchRequestDTO(
|
$fieldMappings = [
|
||||||
fieldMappings: [
|
new \App\Services\InfoProviderSystem\DTOs\FieldMappingDTO('name', ['lcsc'], 1)
|
||||||
['field' => 'name', 'providers' => ['lcsc'], 'priority' => 1]
|
];
|
||||||
],
|
|
||||||
prefetchDetails: false,
|
|
||||||
parts: [$part]
|
|
||||||
);
|
|
||||||
|
|
||||||
// The service should be able to process the request and throw an exception when no results are found
|
// The service should be able to process the request and throw an exception when no results are found
|
||||||
try {
|
try {
|
||||||
$bulkService->performBulkSearch($request);
|
$bulkService->performBulkSearch([$part], $fieldMappings, false);
|
||||||
$this->fail('Expected RuntimeException to be thrown when no search results are found');
|
$this->fail('Expected RuntimeException to be thrown when no search results are found');
|
||||||
} catch (\RuntimeException $e) {
|
} catch (\RuntimeException $e) {
|
||||||
$this->assertStringContainsString('No search results found', $e->getMessage());
|
$this->assertStringContainsString('No search results found', $e->getMessage());
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue