diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index cb6e94de..c65ca71f 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -24,6 +24,7 @@ namespace App\Entity\AssemblySystem; use App\Repository\AssemblyRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -111,11 +112,12 @@ class Assembly extends AbstractStructuralDBElement * @var Collection */ #[Assert\Valid] + #[AssemblyCycle] + #[AssemblyInvalidBomEntry] + #[UniqueReferencedAssembly] #[Groups(['extended', 'full', 'import'])] #[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)] #[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])] - #[AssemblyCycle] - #[UniqueReferencedAssembly] #[UniqueObjectCollection(message: 'assembly.bom_entry.project_already_in_bom', fields: ['project'])] #[UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name'])] protected Collection $bom_entries; diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 8a4cdbc2..7d54fe68 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -39,6 +39,7 @@ use App\Entity\Contracts\TimeStampableInterface; use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; use App\Validator\Constraints\AssemblySystem\AssemblyCycle; +use App\Validator\Constraints\AssemblySystem\AssemblyInvalidBomEntry; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -142,6 +143,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] #[AssemblyCycle] + #[AssemblyInvalidBomEntry] #[ORM\ManyToOne(targetEntity: Assembly::class)] #[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')] #[Groups(['bom_entry:read', 'bom_entry:write', ])] diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php index 73df284d..f12f19a7 100644 --- a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -49,49 +49,78 @@ class AssemblyCycleValidator extends ConstraintValidator return; } - $bomEntries = $value->getBomEntries()->toArray(); + $availableViolations = $this->context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; - // Consider additional entries from the form if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { - $formBomEntries = $this->context->getRoot()->get('bom_entries')->getData(); - if ($formBomEntries) { - $given = is_array($formBomEntries) ? $formBomEntries : iterator_to_array($formBomEntries); - foreach ($given as $givenIdx => $entry) { - if (in_array($entry, $bomEntries, true)) { - continue; - } else { - $bomEntries[$givenIdx] = $entry; - } - } + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; } } $visitedAssemblies = []; - foreach ($bomEntries as $bomEntry) { + foreach ($relevantEntries as $bomEntry) { if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { $this->addViolation($value, $constraint); } } } - private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + /** + * Determines if there is a cyclic dependency in the assembly hierarchy. + * + * This method checks if a cycle exists in the hierarchy of referenced assemblies starting + * from a given assembly. It traverses through the Bill of Materials (BOM) entries of each + * assembly recursively and keeps track of visited assemblies to detect cycles. + * + * @param Assembly|null $currentAssembly The current assembly being checked for cycles. + * @param Assembly $originalAssembly The original assembly from where the cycle detection started. + * @param Assembly[] $visitedAssemblies A list of assemblies that have been visited during the current traversal. + * + * @return bool True if a cycle is detected, false otherwise. + */ + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array $visitedAssemblies = []): bool { + //No referenced assembly → no cycle if ($currentAssembly === null) { return false; } - if (in_array($currentAssembly, $visitedAssemblies, true)) { + //If the assembly has already been visited, there is a cycle + if (in_array($currentAssembly->getId(), array_map(fn($a) => $a->getId(), $visitedAssemblies), true)) { return true; } + //Add the current assembly to the visited $visitedAssemblies[] = $currentAssembly; + //Go through the bom entries of the current assembly foreach ($currentAssembly->getBomEntries() as $bomEntry) { - if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($referencedAssembly !== null && $this->hasCycle($referencedAssembly, $originalAssembly, $visitedAssemblies)) { return true; } } + //Remove the current assembly from the list of visit (recursion completed) + array_pop($visitedAssemblies); + return false; } @@ -102,11 +131,11 @@ class AssemblyCycleValidator extends ConstraintValidator * already exists in the context. If such a violation is found, the current violation is not added again. * The process involves reflection to access private or protected properties of violation objects. * - * @param mixed $value The value that triggered the violation. - * @param Constraint $constraint The constraint containing the validation details. + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. * */ - private function addViolation($value, Constraint $constraint): void + private function addViolation(mixed $value, Constraint $constraint): void { /** @var ConstraintViolationBuilder $buildViolation */ $buildViolation = $this->context->buildViolation($constraint->message) diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php new file mode 100644 index 00000000..73234c86 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyInvalidBomEntry.php @@ -0,0 +1,21 @@ +context->getViolations(); + if (count($availableViolations) > 0) { + //already violations given, currently no more needed to check + + return; + } + + $bomEntries = []; + + if ($this->context->getRoot() instanceof Form && $this->context->getRoot()->has('bom_entries')) { + $bomEntries = $this->context->getRoot()->get('bom_entries')->getData(); + $bomEntries = is_array($bomEntries) ? $bomEntries : iterator_to_array($bomEntries); + } elseif ($this->context->getRoot() instanceof Assembly) { + $bomEntries = $value->getBomEntries()->toArray(); + } + + $relevantEntries = []; + + foreach ($bomEntries as $bomEntry) { + if ($bomEntry->getReferencedAssembly() !== null) { + $relevantEntries[$bomEntry->getId()] = $bomEntry; + } + } + + foreach ($relevantEntries as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($bomEntry->getAssembly()->getParent()?->getId() === $referencedAssembly->getParent()?->getId()) { + //Save on the same assembly level + continue; + } elseif ($this->isInvalidBomEntry($referencedAssembly, $bomEntry->getAssembly())) { + $this->addViolation($value, $constraint); + } + } + } + + /** + * Determines whether a Bill of Materials (BOM) entry is invalid based on the relationship + * between the current assembly and the parent assembly. + * + * @param Assembly|null $currentAssembly The current assembly being analyzed. Null indicates no assembly is referenced. + * @param Assembly $parentAssembly The parent assembly to check against the current assembly. + * + * @return bool Returns + */ + private function isInvalidBomEntry(?Assembly $currentAssembly, Assembly $parentAssembly): bool + { + //No assembly referenced -> no problems + if ($currentAssembly === null) { + return false; + } + + //Check: is the current assembly a descendant of the parent assembly? + if ($currentAssembly->isChildOf($parentAssembly)) { + return true; + } + + //Recursive check: Analyze the current assembly list + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + $referencedAssembly = $bomEntry->getReferencedAssembly(); + + if ($this->isInvalidBomEntry($referencedAssembly, $parentAssembly)) { + return true; + } + } + + return false; + + } + + private function isOnSameLevel(Assembly $assembly1, Assembly $assembly2): bool + { + $parent1 = $assembly1->getParent(); + $parent2 = $assembly2->getParent(); + + if ($parent1 === null || $parent2 === null) { + return false; + } + + // Beide Assemblies teilen denselben Parent + return $parent1 !== null && $parent2 !== null && $parent1->getId() === $parent2->getId(); + } + + /** + * Adds a violation to the current context if it hasn’t already been added. + * + * This method checks whether a violation with the same property path as the current violation + * already exists in the context. If such a violation is found, the current violation is not added again. + * The process involves reflection to access private or protected properties of violation objects. + * + * @param mixed $value The value that triggered the violation. + * @param Constraint $constraint The constraint containing the validation details. + * + */ + private function addViolation($value, Constraint $constraint): void + { + /** @var ConstraintViolationBuilder $buildViolation */ + $buildViolation = $this->context->buildViolation($constraint->message) + ->setParameter('%name%', $value->getName()); + + $alreadyAdded = false; + + try { + $reflectionClass = new ReflectionClass($buildViolation); + $property = $reflectionClass->getProperty('propertyPath'); + $propertyPath = $property->getValue($buildViolation); + + $availableViolations = $this->context->getViolations(); + + foreach ($availableViolations as $tmpViolation) { + $tmpReflectionClass = new ReflectionClass($tmpViolation); + $tmpProperty = $tmpReflectionClass->getProperty('propertyPath'); + $tmpPropertyPath = $tmpProperty->getValue($tmpViolation); + + if ($tmpPropertyPath === $propertyPath) { + $alreadyAdded = true; + } + } + } catch (\ReflectionException) { + } + + if (!$alreadyAdded) { + $buildViolation->addViolation(); + } + } +} \ No newline at end of file diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 7ee171b2..245e61e3 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -401,6 +401,12 @@ Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + + assembly.bom_entry.invalid_child_entry + Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 24fa330a..f30dd211 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -377,6 +377,12 @@ En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + + assembly.bom_entry.invalid_child_entry + En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 549dfe63..8e6d7f02 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -401,6 +401,12 @@ Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + + assembly.bom_entry.invalid_child_entry + Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index bc9b0947..9464f288 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -43,6 +43,12 @@ Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + + assembly.bom_entry.invalid_child_entry + Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 7d9beb4e..53ad4cde 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -401,6 +401,12 @@ A cycle was detected: the assembly "%name%" indirectly references itself. + + + assembly.bom_entry.invalid_child_entry + An assembly must not reference a subassembly from its own hierarchy in the BOM entries. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index 1c9c5302..44046829 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -233,6 +233,12 @@ Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + + assembly.bom_entry.invalid_child_entry + Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 89d470e7..485cb0e2 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -395,6 +395,12 @@ Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + + assembly.bom_entry.invalid_child_entry + Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index e9b528bb..74d3969f 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -395,6 +395,12 @@ È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + + assembly.bom_entry.invalid_child_entry + Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index 80ec65ff..f9f8a54f 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -233,6 +233,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 5df01cd6..9916178c 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -395,6 +395,12 @@ 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + + assembly.bom_entry.invalid_child_entry + Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 8bf08ab3..b8029e47 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -395,6 +395,12 @@ Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 87c507c1..6e4fc056 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -383,6 +383,12 @@ 检测到循环:装配体“%name%”间接引用了其自身。 + + + assembly.bom_entry.invalid_child_entry + Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM). + + assembly.bom_entry.project_already_in_bom