. */ 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 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() 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; } } } } $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(); } } }