Baugruppen Stückliste um referenzierte Baugruppe erweitern

This commit is contained in:
Marcel Diegelmann 2025-07-03 13:38:51 +02:00
parent 4f9c20a409
commit 4e1c890b5b
48 changed files with 1205 additions and 152 deletions

View file

@ -200,7 +200,7 @@ abstract class BaseAdminController extends AbstractController
* depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one,
* to avoid having to insert it manually */
$entity->setName(str_ireplace('%%ipn%%', $entity->getIpn(), $entity->getName()));
$entity->setName(str_ireplace('%%ipn%%', $entity->getIpn() ?? '', $entity->getName()));
}
$this->commentHelper->setMessage($form['log_comment']->getData());
@ -233,6 +233,13 @@ abstract class BaseAdminController extends AbstractController
$repo = $this->entityManager->getRepository($this->entity_class);
$showParameters = true;
if ($this instanceof AssemblyAdminController) {
//currently not needed for assemblies
$showParameters = false;
}
return $this->render($this->twig_template, [
'entity' => $entity,
'form' => $form,
@ -242,7 +249,7 @@ abstract class BaseAdminController extends AbstractController
'timeTravel' => $timeTravel_timestamp,
'repo' => $repo,
'partsContainingElement' => $repo instanceof PartsContainingRepositoryInterface,
'showParameters' => !($this instanceof AssemblyAdminController),
'showParameters' => $showParameters,
]);
}
@ -303,7 +310,7 @@ abstract class BaseAdminController extends AbstractController
* depending on CREATE_ASSEMBLY_USE_IPN_PLACEHOLDER_IN_NAME, when creating a new one,
* to avoid having to insert it manually */
$new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn(), $new_entity->getName()));
$new_entity->setName(str_ireplace('%%ipn%%', $new_entity->getIpn() ?? '', $new_entity->getName()));
}
$this->commentHelper->setMessage($form['log_comment']->getData());
@ -396,13 +403,20 @@ abstract class BaseAdminController extends AbstractController
}
}
$showParameters = true;
if ($this instanceof AssemblyAdminController) {
//currently not needed for assemblies
$showParameters = false;
}
return $this->render($this->twig_template, [
'entity' => $new_entity,
'form' => $form,
'import_form' => $import_form,
'mass_creation_form' => $mass_creation_form,
'route_base' => $this->route_base,
'showParameters' => !($this instanceof AssemblyAdminController),
'showParameters' => $showParameters,
]);
}

View file

@ -22,8 +22,10 @@ declare(strict_types=1);
namespace App\Controller;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Parameters\AbstractParameter;
use App\Entity\ProjectSystem\Project;
use App\Services\Attachments\AssemblyPreviewGenerator;
use App\Services\Attachments\ProjectPreviewGenerator;
use Symfony\Component\HttpFoundation\Response;
use App\Entity\Attachments\Attachment;
@ -195,6 +197,44 @@ class TypeaheadController extends AbstractController
return new JsonResponse($result);
}
#[Route(path: '/assemblies/search/{query}', name: 'typeahead_assemblies')]
public function assemblies(
EntityManagerInterface $entityManager,
AssemblyPreviewGenerator $assemblyPreviewGenerator,
AttachmentURLGenerator $attachmentURLGenerator,
string $query = ""
): JsonResponse {
$this->denyAccessUnlessGranted('@assemblies.read');
$result = [];
$assemblyRepository = $entityManager->getRepository(Assembly::class);
$assemblies = $assemblyRepository->autocompleteSearch($query, 100);
foreach ($assemblies as $assembly) {
$preview_attachment = $assemblyPreviewGenerator->getTablePreviewAttachment($assembly);
if($preview_attachment instanceof Attachment) {
$preview_url = $attachmentURLGenerator->getThumbnailURL($preview_attachment, 'thumbnail_sm');
} else {
$preview_url = '';
}
/** @var Assembly $assembly */
$result[] = [
'id' => $assembly->getID(),
'name' => $assembly->getName(),
'category' => '',
'footprint' => '',
'description' => mb_strimwidth($assembly->getDescription(), 0, 127, '...'),
'image' => $preview_url,
];
}
return new JsonResponse($result);
}
#[Route(path: '/parameters/{type}/search/{query}', name: 'typeahead_parameters', requirements: ['type' => '.+'])]
public function parameters(string $type, EntityManagerInterface $entityManager, string $query = ""): JsonResponse
{

View file

@ -22,6 +22,7 @@ declare(strict_types=1);
namespace App\Entity\AssemblySystem;
use App\Repository\AssemblyRepository;
use Doctrine\Common\Collections\Criteria;
use ApiPlatform\Doctrine\Orm\Filter\OrderFilter;
use ApiPlatform\Metadata\ApiFilter;
@ -38,6 +39,7 @@ use ApiPlatform\Serializer\Filter\PropertyFilter;
use App\ApiPlatform\Filter\LikeFilter;
use App\Entity\Attachments\Attachment;
use App\Validator\Constraints\UniqueObjectCollection;
use App\Validator\Constraints\AssemblySystem\UniqueReferencedAssembly;
use Doctrine\DBAL\Types\Types;
use App\Entity\Attachments\AssemblyAttachment;
use App\Entity\Base\AbstractStructuralDBElement;
@ -58,7 +60,7 @@ use Symfony\Component\Validator\Context\ExecutionContextInterface;
*
* @extends AbstractStructuralDBElement<AssemblyAttachment, AssemblyParameter>
*/
#[ORM\Entity]
#[ORM\Entity(repositoryClass: AssemblyRepository::class)]
#[ORM\Table(name: 'assemblies')]
#[UniqueEntity(fields: ['ipn'], message: 'assembly.ipn.must_be_unique')]
#[ORM\Index(columns: ['ipn'], name: 'assembly_idx_ipn')]
@ -109,8 +111,9 @@ class Assembly extends AbstractStructuralDBElement
*/
#[Assert\Valid]
#[Groups(['extended', 'full', 'import'])]
#[ORM\OneToMany(mappedBy: 'assembly', targetEntity: AssemblyBOMEntry::class, cascade: ['persist', 'remove'], orphanRemoval: true)]
#[ORM\OneToMany(targetEntity: AssemblyBOMEntry::class, mappedBy: 'assembly', cascade: ['persist', 'remove'], orphanRemoval: true)]
#[UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part'])]
#[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;
@ -386,4 +389,22 @@ class Assembly extends AbstractStructuralDBElement
}
}
}
/**
* Get all referenced assemblies which uses this assembly.
*
* @return Assembly[] all referenced assemblies which uses this assembly as a one-dimensional array of assembly objects
*/
public function getReferencedAssemblies(): array
{
$assemblies = [];
foreach($this->bom_entries as $entry) {
if ($entry->getAssembly() !== null) {
$assemblies[] = $entry->getReferencedAssembly();
}
}
return $assemblies;
}
}

View file

@ -133,6 +133,18 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
#[Groups(['bom_entry:read', 'bom_entry:write', 'full'])]
protected ?Part $part = null;
/**
* @var Assembly|null The associated assembly
*/
#[Assert\Expression(
'(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'
)]
#[ORM\ManyToOne(targetEntity: Assembly::class)]
#[ORM\JoinColumn(name: 'id_referenced_assembly', nullable: true, onDelete: 'SET NULL')]
#[Groups(['bom_entry:read', 'bom_entry:write', ])]
protected ?Assembly $referencedAssembly = null;
/**
* @var Project|null The associated project
*/
@ -237,6 +249,17 @@ class AssemblyBOMEntry extends AbstractDBElement implements UniqueValidatableInt
return $this;
}
public function getReferencedAssembly(): ?Assembly
{
return $this->referencedAssembly;
}
public function setReferencedAssembly(?Assembly $referencedAssembly): AssemblyBOMEntry
{
$this->referencedAssembly = $referencedAssembly;
return $this;
}
public function getProject(): ?Project
{
return $this->project;

View file

@ -51,14 +51,17 @@ class AssemblyAddPartsType extends AbstractType
$builder->add('bom_entries', AssemblyBOMEntryCollectionType::class, [
'entry_options' => [
'constraints' => [
new UniqueEntity(fields: ['part', 'assembly'], message: 'assembly.bom_entry.part_already_in_bom',
new UniqueEntity(fields: ['part'], message: 'assembly.bom_entry.part_already_in_bom',
entityClass: AssemblyBOMEntry::class),
new UniqueEntity(fields: ['name', 'assembly'], message: 'assembly.bom_entry.name_already_in_bom',
new UniqueEntity(fields: ['referencedAssembly'], message: 'assembly.bom_entry.assembly_already_in_bom',
entityClass: AssemblyBOMEntry::class),
new UniqueEntity(fields: ['name'], message: 'assembly.bom_entry.name_already_in_bom',
entityClass: AssemblyBOMEntry::class, ignoreNull: true),
]
],
'constraints' => [
new UniqueObjectCollection(message: 'assembly.bom_entry.part_already_in_bom', fields: ['part']),
new UniqueObjectCollection(message: 'assembly.bom_entry.assembly_already_in_bom', fields: ['referencedAssembly']),
new UniqueObjectCollection(message: 'assembly.bom_entry.name_already_in_bom', fields: ['name']),
]
]);

View file

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Form\AssemblySystem;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use App\Form\Type\AssemblySelectType;
use App\Form\Type\BigDecimalNumberType;
use App\Form\Type\CurrencyEntityType;
use App\Form\Type\PartSelectType;
@ -42,6 +43,10 @@ class AssemblyBOMEntryType extends AbstractType
'label' => 'assembly.bom.project',
'required' => false,
])
->add('referencedAssembly', AssemblySelectType::class, [
'label' => 'assembly.bom.referencedAssembly',
'required' => false,
])
->add('name', TextType::class, [
'label' => 'assembly.bom.name',
'required' => false,

View file

@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace App\Form\Type;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\Attachment;
use App\Services\Attachments\AssemblyPreviewGenerator;
use App\Services\Attachments\AttachmentURLGenerator;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\ChoiceList\ChoiceList;
use Symfony\Component\Form\DataMapperInterface;
use Symfony\Component\Form\Event\PreSetDataEvent;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\FormEvent;
use Symfony\Component\Form\FormEvents;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class AssemblySelectType extends AbstractType implements DataMapperInterface
{
public function __construct(private readonly UrlGeneratorInterface $urlGenerator, private readonly EntityManagerInterface $em, private readonly AssemblyPreviewGenerator $previewGenerator, private readonly AttachmentURLGenerator $attachmentURLGenerator)
{
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
//At initialization, we have to fill the form element with our selected data, so the user can see it
$builder->addEventListener(FormEvents::PRE_SET_DATA, function (PreSetDataEvent $event) {
$form = $event->getForm();
$config = $form->getConfig()->getOptions();
$data = $event->getData() ?? [];
$config['compound'] = false;
$config['choices'] = is_iterable($data) ? $data : [$data];
$config['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $config);
});
//After form submit, we have to add the selected element as choice, otherwise the form will not accept this element
$builder->addEventListener(FormEvents::PRE_SUBMIT, function(FormEvent $event) {
$data = $event->getData();
$form = $event->getForm();
$options = $form->get('autocomplete')->getConfig()->getOptions();
if (!isset($data['autocomplete']) || '' === $data['autocomplete'] || empty($data['autocomplete'])) {
$options['choices'] = [];
} else {
//Extract the ID from the submitted data
$id = $data['autocomplete'];
//Find the element in the database
$element = $this->em->find($options['class'], $id);
//Add the element as choice
$options['choices'] = [$element];
$options['error_bubbling'] = true;
$form->add('autocomplete', EntityType::class, $options);
}
});
$builder->setDataMapper($this);
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'class' => Assembly::class,
'choice_label' => 'name',
'compound' => true,
'error_bubbling' => false,
]);
error_log($this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']));
$resolver->setDefaults([
'attr' => [
'data-controller' => 'elements--assembly-select',
'data-autocomplete' => $this->urlGenerator->generate('typeahead_assemblies', ['query' => '__QUERY__']),
'autocomplete' => 'off',
],
]);
$resolver->setDefaults([
//Prefill the selected choice with the needed data, so the user can see it without an additional Ajax request
'choice_attr' => ChoiceList::attr($this, function (?Assembly $assembly) {
if($assembly instanceof Assembly) {
//Determine the picture to show:
$preview_attachment = $this->previewGenerator->getTablePreviewAttachment($assembly);
if ($preview_attachment instanceof Attachment) {
$preview_url = $this->attachmentURLGenerator->getThumbnailURL($preview_attachment,
'thumbnail_sm');
} else {
$preview_url = '';
}
}
return $assembly instanceof Assembly ? [
'data-description' => $assembly->getDescription() ? mb_strimwidth($assembly->getDescription(), 0, 127, '...') : '',
'data-category' => '',
'data-footprint' => '',
'data-image' => $preview_url,
] : [];
})
]);
}
public function mapDataToForms($data, \Traversable $forms): void
{
$form = current(iterator_to_array($forms, false));
$form->setData($data);
}
public function mapFormsToData(\Traversable $forms, &$data): void
{
$form = current(iterator_to_array($forms, false));
$data = $form->getData();
}
}

View file

@ -0,0 +1,69 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Repository;
use App\Entity\AssemblySystem\Assembly;
/**
* @template TEntityClass of Assembly
* @extends DBElementRepository<TEntityClass>
*/
class AssemblyRepository extends StructuralDBElementRepository
{
/**
* @return Assembly[]
*/
public function autocompleteSearch(string $query, int $max_limits = 50): array
{
$qb = $this->createQueryBuilder('assembly');
$qb->select('assembly')
->where('ILIKE(assembly.name, :query) = TRUE')
->orWhere('ILIKE(assembly.description, :query) = TRUE');
$qb->setParameter('query', '%'.$query.'%');
$qb->setMaxResults($max_limits);
$qb->orderBy('NATSORT(assembly.name)', 'ASC');
return $qb->getQuery()->getResult();
}
}

View file

@ -67,6 +67,7 @@ class AssemblyBuildHelper
public function getMaximumBuildableCount(Assembly $assembly): int
{
$maximum_buildable_count = PHP_INT_MAX;
/** @var AssemblyBOMEntry $bom_entry */
foreach ($assembly->getBomEntries() as $bom_entry) {
//Skip BOM entries without a part (as we can not determine that)
if (!$bom_entry->isPartBomEntry() && $bom_entry->getProject() === null) {
@ -76,8 +77,8 @@ class AssemblyBuildHelper
//The maximum buildable count for the whole project is the minimum of all BOM entries
if ($bom_entry->getPart() !== null) {
$maximum_buildable_count = min($maximum_buildable_count, $this->getMaximumBuildableCountForBOMEntry($bom_entry));
} elseif ($bom_entry->getProject() !== null) {
$maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getProject()));
} elseif ($bom_entry->getReferencedAssembly() !== null) {
$maximum_buildable_count = min($maximum_buildable_count, $this->projectBuildHelper->getMaximumBuildableCount($bom_entry->getReferencedAssembly()));
}
}
@ -117,11 +118,12 @@ class AssemblyBuildHelper
$nonBuildableEntries = [];
/** @var AssemblyBOMEntry $bomEntry */
foreach ($assembly->getBomEntries() as $bomEntry) {
$part = $bomEntry->getPart();
//Skip BOM entries without a part (as we can not determine that)
if (!$part instanceof Part && $bomEntry->getAssembly() === null) {
if (!$part instanceof Part && $bomEntry->getReferencedAssembly() === null) {
continue;
}
@ -131,8 +133,8 @@ class AssemblyBuildHelper
if ($amount_sum < $bomEntry->getQuantity() * $number_of_builds) {
$nonBuildableEntries[] = $bomEntry;
}
} elseif ($bomEntry->getAssembly() !== null) {
$nonBuildableAssemblyEntries = $this->projectBuildHelper->getNonBuildableProjectBomEntries($bomEntry->getProject(), $number_of_builds);
} elseif ($bomEntry->getReferencedAssembly() !== null) {
$nonBuildableAssemblyEntries = $this->getNonBuildableAssemblyBomEntries($bomEntry->getReferencedAssembly(), $number_of_builds);
$nonBuildableEntries = array_merge($nonBuildableEntries, $nonBuildableAssemblyEntries);
}
}

View file

@ -0,0 +1,93 @@
<?php
/**
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Services\Attachments;
use App\Entity\AssemblySystem\Assembly;
use App\Entity\Attachments\Attachment;
class AssemblyPreviewGenerator
{
public function __construct(protected AttachmentManager $attachmentHelper)
{
}
/**
* Returns a list of attachments that can be used for previewing the assembly ordered by priority.
*
* @param Assembly $assembly the assembly for which the attachments should be determined
*
* @return (Attachment|null)[]
*
* @psalm-return list<Attachment|null>
*/
public function getPreviewAttachments(Assembly $assembly): array
{
$list = [];
//Master attachment has top priority
$attachment = $assembly->getMasterPictureAttachment();
if ($this->isAttachmentValidPicture($attachment)) {
$list[] = $attachment;
}
//Then comes the other images of the assembly
foreach ($assembly->getAttachments() as $attachment) {
//Dont show the master attachment twice
if ($this->isAttachmentValidPicture($attachment) && $attachment !== $assembly->getMasterPictureAttachment()) {
$list[] = $attachment;
}
}
return $list;
}
/**
* Determines what attachment should be used for previewing a assembly (especially in assembly table).
* The returned attachment is guaranteed to be existing and be a picture.
*
* @param Assembly $assembly The assembly for which the attachment should be determined
*/
public function getTablePreviewAttachment(Assembly $assembly): ?Attachment
{
$attachment = $assembly->getMasterPictureAttachment();
if ($this->isAttachmentValidPicture($attachment)) {
return $attachment;
}
return null;
}
/**
* Checks if a attachment is exising and a valid picture.
*
* @param Attachment|null $attachment the attachment that should be checked
*
* @return bool true if the attachment is valid
*/
protected function isAttachmentValidPicture(?Attachment $attachment): bool
{
return $attachment instanceof Attachment
&& $attachment->isPicture()
&& $this->attachmentHelper->isFileExisting($attachment);
}
}

View file

@ -188,6 +188,15 @@ class TreeViewGenerator
$root_node->setExpanded($this->rootNodeExpandedByDefault);
$root_node->setIcon($this->entityClassToRootNodeIcon($class));
$generic = [$root_node];
} elseif ($mode === 'assemblies' && $this->rootNodeEnabled) {
//We show the root node as a link to the list of all assemblies
$show_all_parts_url = $this->router->generate('assemblies_list');
$root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic);
$root_node->setExpanded($this->rootNodeExpandedByDefault);
$root_node->setIcon($this->entityClassToRootNodeIcon($class));
$generic = [$root_node];
}

View file

@ -2,6 +2,7 @@
namespace App\Twig;
use App\Entity\AssemblySystem\AssemblyBOMEntry;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;
@ -14,6 +15,9 @@ class AssemblyTwigExtension extends AbstractExtension
];
}
/**
* @param AssemblyBOMEntry[] $bomEntries
*/
public function hasProject(array $bomEntries): bool
{
foreach ($bomEntries as $entry) {

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Validator\Constraints\AssemblySystem;
use Symfony\Component\Validator\Constraint;
/**
* This constraint checks that the given UniqueReferencedAssembly is valid.
*/
#[\Attribute(\Attribute::TARGET_PROPERTY)]
class UniqueReferencedAssembly extends Constraint
{
public string $message = 'assembly.bom_entry.assembly_already_in_bom';
}

View file

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
namespace App\Validator\Constraints\AssemblySystem;
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
class UniqueReferencedAssemblyValidator extends ConstraintValidator
{
public function validate($value, Constraint $constraint)
{
$assemblies = [];
foreach ($value as $entry) {
$referencedAssemblyId = $entry->getReferencedAssembly()?->getId();
if ($referencedAssemblyId === null) {
continue;
}
if (isset($assemblies[$referencedAssemblyId])) {
$this->context->buildViolation($constraint->message)
->atPath('referencedAssembly')
->addViolation();
return;
}
$assemblies[$referencedAssemblyId] = true;
}
}
}