diff --git a/config/services.yaml b/config/services.yaml index 8256daa1..659098ff 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -168,6 +168,9 @@ services: arguments: $useAssemblyIpnPlaceholder: '%partdb.create_assembly_use_ipn_placeholder_in_name%' + App\Validator\Constraints\AssemblySystem\AssemblyCycleValidator: + tags: [ 'validator.constraint_validator' ] + #################################################################################################################### # Table settings #################################################################################################################### diff --git a/src/Entity/AssemblySystem/Assembly.php b/src/Entity/AssemblySystem/Assembly.php index 9593dbb5..5ce060d8 100644 --- a/src/Entity/AssemblySystem/Assembly.php +++ b/src/Entity/AssemblySystem/Assembly.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Entity\AssemblySystem; use App\Repository\AssemblyRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use Doctrine\Common\Collections\Criteria; use ApiPlatform\Doctrine\Orm\Filter\OrderFilter; use ApiPlatform\Metadata\ApiFilter; @@ -113,6 +114,7 @@ class Assembly extends AbstractStructuralDBElement #[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'])] diff --git a/src/Entity/AssemblySystem/AssemblyBOMEntry.php b/src/Entity/AssemblySystem/AssemblyBOMEntry.php index 820fc2f5..6a3e82d3 100644 --- a/src/Entity/AssemblySystem/AssemblyBOMEntry.php +++ b/src/Entity/AssemblySystem/AssemblyBOMEntry.php @@ -38,6 +38,7 @@ use App\ApiPlatform\Filter\LikeFilter; use App\Entity\Contracts\TimeStampableInterface; use App\Entity\ProjectSystem\Project; use App\Repository\DBElementRepository; +use App\Validator\Constraints\AssemblySystem\AssemblyCycle; use App\Validator\UniqueValidatableInterface; use Doctrine\DBAL\Types\Types; use App\Entity\Base\AbstractDBElement; @@ -140,6 +141,7 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt '(this.getPart() === null or this.getReferencedAssembly() === null) and (this.getName() === null or (this.getName() != null and this.getName() != ""))', message: 'validator.assembly.bom_entry.only_part_or_assembly_allowed' )] + #[AssemblyCycle] #[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/AssemblyCycle.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php new file mode 100644 index 00000000..9d79b879 --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycle.php @@ -0,0 +1,39 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use Symfony\Component\Validator\Constraint; + +/** + * This constraint checks that there is no cycle in bom configuration of the assembly + */ +#[\Attribute(\Attribute::TARGET_PROPERTY)] +class AssemblyCycle extends Constraint +{ + public string $message = 'assembly.bom_entry.assembly_cycle'; + + public function validatedBy(): string + { + return AssemblyCycleValidator::class; + } +} \ No newline at end of file diff --git a/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php new file mode 100644 index 00000000..3483f94a --- /dev/null +++ b/src/Validator/Constraints/AssemblySystem/AssemblyCycleValidator.php @@ -0,0 +1,139 @@ +. + */ +namespace App\Validator\Constraints\AssemblySystem; + +use App\Entity\AssemblySystem\Assembly; +use Symfony\Component\Validator\Constraint; +use Symfony\Component\Validator\ConstraintValidator; +use Symfony\Component\Validator\Exception\UnexpectedTypeException; +use Symfony\Component\Validator\Violation\ConstraintViolationBuilder; +use ReflectionClass; + +/** + * Validator class to check for cycles in assemblies based on BOM entries. + * + * This validator ensures that the structure of assemblies does not contain circular dependencies + * by validating each entry in the Bill of Materials (BOM) of the given assembly. Additionally, + * it can handle form-submitted BOM entries to include these in the validation process. + */ +class AssemblyCycleValidator extends ConstraintValidator +{ + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof AssemblyCycle) { + throw new UnexpectedTypeException($constraint, AssemblyCycle::class); + } + + if (!$value instanceof Assembly) { + return; + } + + $bomEntries = $value->getBomEntries()->toArray(); + + // Consider additional entries from the form + if ($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; + } + } + } + } + + $visitedAssemblies = []; + foreach ($bomEntries as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $value, $visitedAssemblies)) { + $this->addViolation($value, $constraint); + } + } + } + + private function hasCycle(?Assembly $currentAssembly, Assembly $originalAssembly, array &$visitedAssemblies): bool + { + if ($currentAssembly === null) { + return false; + } + + if (in_array($currentAssembly, $visitedAssemblies, true)) { + return true; + } + + $visitedAssemblies[] = $currentAssembly; + + foreach ($currentAssembly->getBomEntries() as $bomEntry) { + if ($this->hasCycle($bomEntry->getReferencedAssembly(), $originalAssembly, $visitedAssemblies)) { + return true; + } + } + + return false; + } + + /** + * 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 1731c90c..7ee171b2 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -395,6 +395,12 @@ Tato sestava již existuje jako položka v seznamu materiálů! + + + assembly.bom_entry.assembly_cycle + Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.da.xlf b/translations/validators.da.xlf index 239e3572..24fa330a 100644 --- a/translations/validators.da.xlf +++ b/translations/validators.da.xlf @@ -371,6 +371,12 @@ Denne samling findes allerede som en post! + + + assembly.bom_entry.assembly_cycle + En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 3c3abb2b..549dfe63 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -395,6 +395,12 @@ Diese Baugruppe existiert bereits als Eintrag! + + + assembly.bom_entry.assembly_cycle + Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.el.xlf b/translations/validators.el.xlf index 4e4278da..bc9b0947 100644 --- a/translations/validators.el.xlf +++ b/translations/validators.el.xlf @@ -37,6 +37,12 @@ Αυτή η συναρμολόγηση υπάρχει ήδη ως εγγραφή! + + + assembly.bom_entry.assembly_cycle + Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 59cabf55..7d9beb4e 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -395,6 +395,12 @@ This assembly already exists as an entry! + + + assembly.bom_entry.assembly_cycle + A cycle was detected: the assembly "%name%" indirectly references itself. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.fr.xlf b/translations/validators.fr.xlf index aff68a18..1c9c5302 100644 --- a/translations/validators.fr.xlf +++ b/translations/validators.fr.xlf @@ -227,6 +227,12 @@ Cet assemblage existe déjà en tant qu'entrée ! + + + assembly.bom_entry.assembly_cycle + Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.hr.xlf b/translations/validators.hr.xlf index 1ee5c06f..89d470e7 100644 --- a/translations/validators.hr.xlf +++ b/translations/validators.hr.xlf @@ -389,6 +389,12 @@ Ova se montaža već nalazi kao zapis! + + + assembly.bom_entry.assembly_cycle + Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.it.xlf b/translations/validators.it.xlf index ac57a2cc..e9b528bb 100644 --- a/translations/validators.it.xlf +++ b/translations/validators.it.xlf @@ -389,6 +389,12 @@ Questo assemblaggio è già presente come voce! + + + assembly.bom_entry.assembly_cycle + È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ja.xlf b/translations/validators.ja.xlf index a316707a..80ec65ff 100644 --- a/translations/validators.ja.xlf +++ b/translations/validators.ja.xlf @@ -227,6 +227,12 @@ このアセンブリはすでにエントリとして存在します! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.pl.xlf b/translations/validators.pl.xlf index 95c44ab4..5df01cd6 100644 --- a/translations/validators.pl.xlf +++ b/translations/validators.pl.xlf @@ -389,6 +389,12 @@ To zestawienie jest już dodane jako wpis! + + + assembly.bom_entry.assembly_cycle + 循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。 + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.ru.xlf b/translations/validators.ru.xlf index 425ede5f..8bf08ab3 100644 --- a/translations/validators.ru.xlf +++ b/translations/validators.ru.xlf @@ -389,6 +389,12 @@ Этот сборочный узел уже добавлен как запись! + + + assembly.bom_entry.assembly_cycle + Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя. + + assembly.bom_entry.project_already_in_bom diff --git a/translations/validators.zh.xlf b/translations/validators.zh.xlf index 4a02523b..87c507c1 100644 --- a/translations/validators.zh.xlf +++ b/translations/validators.zh.xlf @@ -377,6 +377,12 @@ 此装配已经作为条目存在! + + + assembly.bom_entry.assembly_cycle + 检测到循环:装配体“%name%”间接引用了其自身。 + + assembly.bom_entry.project_already_in_bom