BOMValidator: Validierung für rekursive Baugruppen-Eintragsprüfung ergänzt

Es wurde eine neue Validierung hinzugefügt, um sicherzustellen, dass keine Baugruppe in ihrer eigenen Hierarchie als Unterbaugruppe referenziert wird. Diese Logik wurde in die entsprechenden Dateien integriert und unterstützt Mehrsprachigkeit durch neue Übersetzungen.
This commit is contained in:
Marcel Diegelmann 2025-07-24 09:11:28 +02:00
parent 9acca25ac7
commit a62866dfe3
17 changed files with 302 additions and 21 deletions

View file

@ -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<int, AssemblyBOMEntry>
*/
#[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;

View file

@ -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', ])]

View file

@ -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)

View file

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Validator\Constraints\AssemblySystem;
use Symfony\Component\Validator\Constraint;
/**
* This constraint ensures that no BOM entries in the assembly reference its own children.
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class AssemblyInvalidBomEntry extends Constraint
{
public string $message = 'assembly.bom_entry.invalid_child_entry';
public function validatedBy(): string
{
return AssemblyInvalidBomEntryValidator::class;
}
}

View file

@ -0,0 +1,155 @@
<?php
declare(strict_types=1);
namespace App\Validator\Constraints\AssemblySystem;
use App\Entity\AssemblySystem\Assembly;
use Symfony\Component\Form\Form;
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 to check that no child assemblies are referenced in BOM entries.
*/
class AssemblyInvalidBomEntryValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint): void
{
if (!$constraint instanceof AssemblyInvalidBomEntry) {
throw new UnexpectedTypeException($constraint, AssemblyInvalidBomEntry::class);
}
if (!$value instanceof Assembly) {
return;
}
$availableViolations = $this->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 hasnt 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();
}
}
}

View file

@ -401,6 +401,12 @@
<target>Byl zjištěn cyklus: Sestava "%name%" nepřímo odkazuje sama na sebe.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Sestava nesmí ve svém seznamu materiálů (BOM) odkazovat na podskupinu, která je součástí její vlastní hierarchie.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -377,6 +377,12 @@
<target>En cyklus blev opdaget: Samlingen "%name%" refererer indirekte til sig selv.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>En samling må ikke referere til en undergruppe fra sin egen hierarki i BOM-listerne.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -401,6 +401,12 @@
<target>Ein Zyklus wurde entdeckt: Die Baugruppe "%name%" referenziert sich indirekt selbst.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Eine Baugruppe darf keine Unterbaugruppe aus seiner eigenen Hierarchie in den BOM-Einträgen referenzieren.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -43,6 +43,12 @@
<target>Εντοπίστηκε κύκλος: Η συναρμολόγηση "%name%" αναφέρεται έμμεσα στον εαυτό της.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Μία συναρμολόγηση δεν πρέπει να αναφέρεται σε μία υποσυναρμολόγηση από την ίδια την ιεραρχία της στη λίστα BOM.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -401,6 +401,12 @@
<target>A cycle was detected: the assembly "%name%" indirectly references itself.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>An assembly must not reference a subassembly from its own hierarchy in the BOM entries.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -233,6 +233,12 @@
<target>Un cycle a été détecté : L'assemblage "%name%" se réfère indirectement à lui-même.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Un assemblage ne doit pas référencer un sous-assemblage de sa propre hiérarchie dans les entrées de la nomenclature (BOM).</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -395,6 +395,12 @@
<target>Otkriven je ciklus: Sklop "%name%" neizravno referencira samog sebe.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Sklop ne smije referencirati podsklop iz vlastite hijerarhije u unosima BOM-a.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -395,6 +395,12 @@
<target>È stato rilevato un ciclo: L'assemblaggio "%name%" fa riferimento indirettamente a sé stesso.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Un assemblaggio non deve fare riferimento a un sottoassemblaggio nella propria gerarchia nelle voci della distinta base (BOM).</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -233,6 +233,12 @@
<target>循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>アセンブリは、BOMエントリで自身の階層内のサブアセンブリを参照してはいけません。</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -395,6 +395,12 @@
<target>循環が検出されました: アセンブリ「%name%」が間接的に自身を参照しています。</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Zespół nie może odwoływać się do podzespołu w swojej własnej hierarchii w wpisach BOM.</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -395,6 +395,12 @@
<target>Обнаружен цикл: Сборка «%name%» косвенно ссылается на саму себя.</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM).</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>

View file

@ -383,6 +383,12 @@
<target>检测到循环:装配体“%name%”间接引用了其自身。</target>
</segment>
</unit>
<unit id="cEtB87a" name="assembly.bom_entry.invalid_child_entry">
<segment>
<source>assembly.bom_entry.invalid_child_entry</source>
<target>Сборка не должна ссылаться на подсборку внутри своей собственной иерархии в записях спецификации (BOM).</target>
</segment>
</unit>
<unit id="6bkQ3bo" name="assembly.bom_entry.project_already_in_bom">
<segment>
<source>assembly.bom_entry.project_already_in_bom</source>