BOMImporter und AssemblyCycleValidator: Verbesserte Import-Logik

Die Importmethoden wurden vereinheitlicht, um Projekte und Baugruppen gleichermaßen zu unterstützen. Zudem wurde die Validierung optimiert und die CSV-, JSON- und KiCAD-Parsing-Methoden angepasst, um kontextbasierte BOM-Einträge effizienter zu erstellen.
This commit is contained in:
Marcel Diegelmann 2025-07-22 15:52:57 +02:00
parent 06373b3e5a
commit 9acca25ac7
2 changed files with 123 additions and 60 deletions

View file

@ -114,7 +114,7 @@ class BOMImporter
*/ */
public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult public function importFileIntoProject(UploadedFile $file, Project $project, array $options): ImporterResult
{ {
$importerResult = $this->fileToImporterResult($file, $options); $importerResult = $this->fileToImporterResult($project, $file, $options);
if ($importerResult->getViolations()->count() === 0) { if ($importerResult->getViolations()->count() === 0) {
//Assign the bom_entries to the project //Assign the bom_entries to the project
@ -127,12 +127,21 @@ class BOMImporter
} }
/** /**
* Converts the given file into an ImporterResult with an array of BOM entries using the given options and save them into the given assembly. * Imports a file into an Assembly object and processes its contents.
* The changes are not saved into the database yet. *
* This method converts the provided file into an ImporterResult object that contains BOM entries and potential
* validation violations. If no violations are found, the BOM entries extracted from the file are added to the
* provided Assembly object.
*
* @param UploadedFile $file The file to be imported and processed.
* @param Assembly $assembly The target Assembly object to which the BOM entries are added.
* @param array $options Options or configurations related to the import process.
*
* @return ImporterResult An object containing the result of the import process, including BOM entries and any violations.
*/ */
public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult public function importFileIntoAssembly(UploadedFile $file, Assembly $assembly, array $options): ImporterResult
{ {
$importerResult = $this->fileToImporterResult($file, $options, AssemblyBOMEntry::class); $importerResult = $this->fileToImporterResult($assembly, $file, $options);
if ($importerResult->getViolations()->count() === 0) { if ($importerResult->getViolations()->count() === 0) {
//Assign the bom_entries to the assembly //Assign the bom_entries to the assembly
@ -145,12 +154,20 @@ class BOMImporter
} }
/** /**
* Converts the given file into an array of BOM entries using the given options. * Converts the content of a file into an array of BOM (Bill of Materials) entries.
* @return ProjectBOMEntry[]|AssemblyBOMEntry[] *
* This method processes the content of the provided file and delegates the conversion
* to a helper method that generates BOM entries based on the provided import object and options.
*
* @param Project|Assembly $importObject The object determining the context of the BOM entries (either a Project or Assembly).
* @param File $file The file whose content will be converted into BOM entries.
* @param array $options Additional options or configurations to be applied during the conversion process.
*
* @return array An array of BOM entries created from the file content.
*/ */
public function fileToBOMEntries(File $file, array $options, string $objectType = ProjectBOMEntry::class): array public function fileToBOMEntries(Project|Assembly $importObject, File $file, array $options): array
{ {
return $this->stringToBOMEntries($file->getContent(), $options, $objectType); return $this->stringToBOMEntries($importObject, $file->getContent(), $options);
} }
/** /**
@ -171,9 +188,19 @@ class BOMImporter
} }
/** /**
* Converts the given file into an ImporterResult with an array of BOM entries using the given options. * Handles the conversion of an uploaded file into an ImporterResult for a given project or assembly.
*
* This method processes the uploaded file by validating its file extension based on the provided import type
* options and then proceeds to convert the file content into an ImporterResult. If the file extension is
* invalid or unsupported, the result will contain a corresponding violation.
*
* @param Project|Assembly $importObject The context of the import operation (either a Project or Assembly).
* @param UploadedFile $file The uploaded file to be processed.
* @param array $options An array of options, expected to include an 'type' key to determine valid file types.
*
* @return ImporterResult An object containing the results of the import process, including any detected violations.
*/ */
public function fileToImporterResult(UploadedFile $file, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult public function fileToImporterResult(Project|Assembly $importObject, UploadedFile $file, array $options): ImporterResult
{ {
$result = new ImporterResult(); $result = new ImporterResult();
@ -203,7 +230,7 @@ class BOMImporter
$fileExtension, $fileExtension,
[ [
'%extension%' => $fileExtension, '%extension%' => $fileExtension,
'%importType%' => $this->translator->trans($objectType === ProjectBOMEntry::class ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']), '%importType%' => $this->translator->trans($importObject instanceof Project ? 'project.bom_import.type.'.$options['type'] : 'assembly.bom_import.type.'.$options['type']),
'%allowedExtensions%' => implode(', ', $validExtensions), '%allowedExtensions%' => implode(', ', $validExtensions),
] ]
)); ));
@ -211,16 +238,19 @@ class BOMImporter
return $result; return $result;
} }
return $this->stringToImporterResult($file->getContent(), $options, $objectType); return $this->stringToImporterResult($importObject, $file->getContent(), $options);
} }
/** /**
* Import string data into an array of BOM entries, which are not yet assigned to a project. * Import string data into an array of BOM entries, which are not yet assigned to a project.
* @param string $data The data to import *
* @param array $options An array of options * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $data The data to import
* @param array $options An array of options
*
* @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries * @return ProjectBOMEntry[]|AssemblyBOMEntry[] An array of imported entries
*/ */
public function stringToBOMEntries(string $data, array $options, string $objectType = ProjectBOMEntry::class): array public function stringToBOMEntries(Project|Assembly $importObject, string $data, array $options): array
{ {
$resolver = new OptionsResolver(); $resolver = new OptionsResolver();
$resolver = $this->configureOptions($resolver); $resolver = $this->configureOptions($resolver);
@ -234,11 +264,14 @@ class BOMImporter
/** /**
* Import string data into an array of BOM entries, which are not yet assigned to a project. * Import string data into an array of BOM entries, which are not yet assigned to a project.
* @param string $data The data to import *
* @param array $options An array of options * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $data The data to import
* @param array $options An array of options
*
* @return ImporterResult An result of imported entries or a violation list * @return ImporterResult An result of imported entries or a violation list
*/ */
public function stringToImporterResult(string $data, array $options, string $objectType = ProjectBOMEntry::class): ImporterResult public function stringToImporterResult(Project|Assembly $importObject, string $data, array $options): ImporterResult
{ {
$resolver = new OptionsResolver(); $resolver = new OptionsResolver();
$resolver = $this->configureOptions($resolver); $resolver = $this->configureOptions($resolver);
@ -251,14 +284,28 @@ class BOMImporter
)); ));
return match ($options['type']) { return match ($options['type']) {
self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $objectType), self::IMPORT_TYPE_KICAD_PCB => $this->parseKiCADPCB($data, $importObject),
self::IMPORT_TYPE_JSON => $this->parseJson($data, $objectType), self::IMPORT_TYPE_JSON => $this->parseJson($data, $importObject),
self::IMPORT_TYPE_CSV => $this->parseCsv($data, $objectType), self::IMPORT_TYPE_CSV => $this->parseCsv($data, $importObject),
default => $defaultImporterResult, default => $defaultImporterResult,
}; };
} }
private function parseKiCADPCB(string $data, string $objectType = ProjectBOMEntry::class): ImporterResult /**
* Parses a KiCAD PCB file and imports its BOM (Bill of Materials) entries into the given Project or Assembly context.
*
* This method processes a semicolon-delimited CSV data string, normalizes column names,
* validates the required fields, and creates BOM entries for each record in the data.
* The BOM entries are added to the provided Project or Assembly, depending on the context.
*
* @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $data The semicolon- or comma-delimited CSV data to be parsed
*
* @return ImporterResult The result of the import process, containing the created BOM entries.
*
* @throws UnexpectedValueException If required fields are missing in the provided data.
*/
private function parseKiCADPCB(string $data, Project|Assembly $importObject): ImporterResult
{ {
$result = new ImporterResult(); $result = new ImporterResult();
@ -284,8 +331,8 @@ class BOMImporter
throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!'); throw new \UnexpectedValueException('Quantity missing at line ' . ($offset + 1) . '!');
} }
$bom_entry = $objectType === ProjectBOMEntry::class ? new ProjectBOMEntry() : new AssemblyBOMEntry(); $bom_entry = $importObject instanceof Project ? new ProjectBOMEntry() : new AssemblyBOMEntry();
if ($objectType === ProjectBOMEntry::class) { if ($bom_entry instanceof ProjectBOMEntry) {
$bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')'); $bom_entry->setName($entry['Designation'] . ' (' . $entry['Package'] . ')');
} else { } else {
$bom_entry->setName($entry['Designation']); $bom_entry->setName($entry['Designation']);
@ -383,15 +430,15 @@ class BOMImporter
* - Checking for empty or invalid descriptions. * - Checking for empty or invalid descriptions.
* - Ensuring manufacturers, if specified, have valid `name` or `id` values. * - Ensuring manufacturers, if specified, have valid `name` or `id` values.
* *
* @param string $data JSON encoded string containing BOM entries data. * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $objectType The type of entries expected during import (e.g., `ProjectBOMEntry` or `AssemblyBOMEntry`). * @param string $data JSON encoded string containing BOM entries data.
* *
* @return ImporterResult The result containing parsed data and any violations encountered during the parsing process. * @return ImporterResult The result containing parsed data and any violations encountered during the parsing process.
*/ */
private function parseJson(string $data, array $options = [], string $objectType = ProjectBOMEntry::class): ImporterResult private function parseJson(Project|Assembly $importObject, string $data): ImporterResult
{ {
$result = new ImporterResult(); $result = new ImporterResult();
$this->jsonRoot = 'JSON Import for '.$objectType === ProjectBOMEntry::class ? 'Project' : 'Assembly'; $this->jsonRoot = 'JSON Import for '.($importObject instanceof Project ? 'Project' : 'Assembly');
$data = json_decode($data, true); $data = json_decode($data, true);
@ -420,9 +467,9 @@ class BOMImporter
} }
if (isset($entry['part'])) { if (isset($entry['part'])) {
$this->processPart($entry, $result, $key, $objectType,self::IMPORT_TYPE_JSON); $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_JSON);
} else { } else {
$bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null);
$bomEntry->setQuantity((float) $entry['quantity']); $bomEntry->setQuantity((float) $entry['quantity']);
$result->addBomEntry($bomEntry); $result->addBomEntry($bomEntry);
@ -438,16 +485,16 @@ class BOMImporter
* performing validations and converting data based on the provided headers. * performing validations and converting data based on the provided headers.
* Handles potential violations and manages the creation of BOM entries based on the given type. * Handles potential violations and manages the creation of BOM entries based on the given type.
* *
* @param string $csvData The raw CSV data to parse, with rows separated by newlines. * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string $objectType The class type to instantiate for BOM entries, defaults to ProjectBOMEntry. * @param string $csvData The raw CSV data to parse, with rows separated by newlines.
* *
* @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered. * @return ImporterResult Returns an ImporterResult instance containing BOM entries and any validation violations encountered.
*/ */
function parseCsv(string $csvData, string $objectType = ProjectBOMEntry::class): ImporterResult function parseCsv(Project|Assembly $importObject, string $csvData): ImporterResult
{ {
$result = new ImporterResult(); $result = new ImporterResult();
$rows = explode("\r\n", trim($csvData)); $rows = explode("\r\n", trim($csvData));
$headers = str_getcsv(array_shift($rows), ','); $headers = str_getcsv(array_shift($rows));
if (count($headers) === 1 && isset($headers[0])) { if (count($headers) === 1 && isset($headers[0])) {
//If only one column was recognized, try fallback with semicolon as a separator //If only one column was recognized, try fallback with semicolon as a separator
@ -456,7 +503,7 @@ class BOMImporter
foreach ($rows as $key => $row) { foreach ($rows as $key => $row) {
$entry = []; $entry = [];
$values = str_getcsv($row, ','); $values = str_getcsv($row);
if (count($values) === 1 || count($values) !== count($headers)) { if (count($values) === 1 || count($values) !== count($headers)) {
//If only one column was recognized, try fallback with semicolon as a separator //If only one column was recognized, try fallback with semicolon as a separator
@ -484,7 +531,7 @@ class BOMImporter
//Check whether the value is numerical //Check whether the value is numerical
if (is_numeric($values[$index])) { if (is_numeric($values[$index])) {
//Convert to integer or float //Convert to integer or float
$temp = (strpos($values[$index], '.') !== false) $temp = (str_contains($values[$index], '.'))
? floatval($values[$index]) ? floatval($values[$index])
: intval($values[$index]); : intval($values[$index]);
} else { } else {
@ -525,9 +572,9 @@ class BOMImporter
} }
if (isset($entry['part'])) { if (isset($entry['part'])) {
$this->processPart($entry, $result, $key, $objectType, self::IMPORT_TYPE_CSV); $this->processPart($importObject, $entry, $result, $key, self::IMPORT_TYPE_CSV);
} else { } else {
$bomEntry = $this->getOrCreateBomEntry($objectType, $entry['name'] ?? null); $bomEntry = $this->getOrCreateBomEntry($importObject, $entry['name'] ?? null);
$bomEntry->setQuantity((float) $entry['quantity'] ?? 0); $bomEntry->setQuantity((float) $entry['quantity'] ?? 0);
$result->addBomEntry($bomEntry); $result->addBomEntry($bomEntry);
@ -544,15 +591,15 @@ class BOMImporter
* to identify corresponding objects in the database. The result is recorded, and violations are * to identify corresponding objects in the database. The result is recorded, and violations are
* logged if issues or discrepancies exist in the validation or database matching process. * logged if issues or discrepancies exist in the validation or database matching process.
* *
* @param array $entry The array representation of the part entry. * @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param ImporterResult $result The result object used for recording validation violations. * @param array $entry The array representation of the part entry.
* @param int $key The index of the entry in the data array. * @param ImporterResult $result The result object used for recording validation violations.
* @param string $objectType The type of object being processed. * @param int $key The index of the entry in the data array.
* @param string $importType The type of import being performed. * @param string $importType The type of import being performed.
* *
* @return void * @return void
*/ */
private function processPart(array $entry, ImporterResult $result, int $key, string $objectType, string $importType): void private function processPart(Project|Assembly $importObject, array $entry, ImporterResult $result, int $key, string $importType): void
{ {
$prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row'; $prefix = $importType === self::IMPORT_TYPE_JSON ? 'entry' : 'row';
@ -777,12 +824,12 @@ class BOMImporter
$part->setCategory($category); $part->setCategory($category);
} }
if ($objectType === AssemblyBOMEntry::class) { if ($importObject instanceof Assembly) {
$bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['part' => $part]); $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'part' => $part]);
if ($bomEntry === null) { if ($bomEntry === null) {
if (isset($entry['name']) && $entry['name'] !== '') { if (isset($entry['name']) && $entry['name'] !== '') {
$bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $entry['name']]); $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['assembly' => $importObject, 'name' => $entry['name']]);
} }
if ($bomEntry === null) { if ($bomEntry === null) {
@ -790,11 +837,11 @@ class BOMImporter
} }
} }
} else { } else {
$bomEntry = $this->projectBOMEntryRepository->findOneBy(['part' => $part]); $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'part' => $part]);
if ($bomEntry === null) { if ($bomEntry === null) {
if (isset($entry['name']) && $entry['name'] !== '') { if (isset($entry['name']) && $entry['name'] !== '') {
$bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $entry['name']]); $bomEntry = $this->projectBOMEntryRepository->findOneBy(['project' => $importObject, 'name' => $entry['name']]);
} }
if ($bomEntry === null) { if ($bomEntry === null) {
@ -841,22 +888,38 @@ class BOMImporter
return $data; return $data;
} }
private function getOrCreateBomEntry(string $objectType, ?string $name) /**
* Retrieves an existing BOM (Bill of Materials) entry by name or creates a new one if not found.
*
* Depending on whether the provided import object is a Project or Assembly, this method attempts to locate
* a corresponding BOM entry in the appropriate repository. If no entry is located, a new BOM entry object
* is instantiated according to the type of the import object.
*
* @param Project|Assembly $importObject The object determining the context of the BOM entry (either a Project or Assembly).
* @param string|null $name The name of the BOM entry to search for or assign to a new entry.
*
* @return ProjectBOMEntry|AssemblyBOMEntry An existing or newly created BOM entry.
*/
private function getOrCreateBomEntry(Project|Assembly $importObject, ?string $name): ProjectBOMEntry|AssemblyBOMEntry
{ {
$bomEntry = null; $bomEntry = null;
//Check whether there is a name //Check whether there is a name
if (!empty($name)) { if (!empty($name)) {
if ($objectType === ProjectBOMEntry::class) { if ($importObject instanceof Project) {
$bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]); $bomEntry = $this->projectBOMEntryRepository->findOneBy(['name' => $name]);
} elseif ($objectType === AssemblyBOMEntry::class) { } else {
$bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]); $bomEntry = $this->assemblyBOMEntryRepository->findOneBy(['name' => $name]);
} }
} }
//If no bom enttry was found, a new object create //If no bom entry was found, a new object create
if ($bomEntry === null) { if ($bomEntry === null) {
$bomEntry = new $objectType(); if ($importObject instanceof Project) {
$bomEntry = new ProjectBOMEntry();
} else {
$bomEntry = new AssemblyBOMEntry();
}
} }
$bomEntry->setName($name); $bomEntry->setName($name);
@ -887,7 +950,6 @@ class BOMImporter
return $out; return $out;
} }
/** /**
* Builds a JSON-based constraint violation. * Builds a JSON-based constraint violation.
* *
@ -895,10 +957,10 @@ class BOMImporter
* The violation includes a message, property path, invalid value, and other contextual information. * The violation includes a message, property path, invalid value, and other contextual information.
* Translations for the violation message can be applied through the translator service. * Translations for the violation message can be applied through the translator service.
* *
* @param string $message The translation key for the validation message. * @param string $message The translation key for the validation message.
* @param string $propertyPath The property path where the violation occurred. * @param string $propertyPath The property path where the violation occurred.
* @param mixed|null $invalidValue The value that caused the violation (optional). * @param mixed|null $invalidValue The value that caused the violation (optional).
* @param array $parameters Additional parameters for message placeholders (default is an empty array). * @param array $parameters Additional parameters for message placeholders (default is an empty array).
* *
* @return ConstraintViolation The created constraint violation object. * @return ConstraintViolation The created constraint violation object.
*/ */

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Validator\Constraints\AssemblySystem; namespace App\Validator\Constraints\AssemblySystem;
use App\Entity\AssemblySystem\Assembly; use App\Entity\AssemblySystem\Assembly;
use Symfony\Component\Form\Form;
use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator; use Symfony\Component\Validator\ConstraintValidator;
use Symfony\Component\Validator\Exception\UnexpectedTypeException; use Symfony\Component\Validator\Exception\UnexpectedTypeException;
@ -51,7 +52,7 @@ class AssemblyCycleValidator extends ConstraintValidator
$bomEntries = $value->getBomEntries()->toArray(); $bomEntries = $value->getBomEntries()->toArray();
// Consider additional entries from the form // Consider additional entries from the form
if ($this->context->getRoot()->has('bom_entries')) { if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) {
$formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData();
if ($formBomEntries) { if ($formBomEntries) {
$given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries);