mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2026-01-13 13:49:33 +00:00
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:
parent
a34589c462
commit
a0900c518a
17 changed files with 302 additions and 21 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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', ])]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue