diff --git a/src/Entity/Base/AbstractStructuralDBElement.php b/src/Entity/Base/AbstractStructuralDBElement.php index af093e1d..457d40c6 100644 --- a/src/Entity/Base/AbstractStructuralDBElement.php +++ b/src/Entity/Base/AbstractStructuralDBElement.php @@ -92,7 +92,7 @@ abstract class AbstractStructuralDBElement extends AttachmentContainingDBElement /** * @var AbstractStructuralDBElement * @NoneOfItsChildren() - * @Groups({"include_parents"}) + * @Groups({"include_parents", "import"}) */ protected $parent = null; diff --git a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php index 8ad0c5e0..0f88787f 100644 --- a/src/Entity/Parts/PartTraits/BasicPropertyTrait.php +++ b/src/Entity/Parts/PartTraits/BasicPropertyTrait.php @@ -65,7 +65,7 @@ trait BasicPropertyTrait * @ORM\JoinColumn(name="id_category", referencedColumnName="id", nullable=false) * @Selectable() * @Assert\NotNull(message="validator.select_valid_category") - * @Groups({"simple", "extended", "full"}) + * @Groups({"simple", "extended", "full", "import"}) */ protected ?Category $category = null; @@ -74,7 +74,7 @@ trait BasicPropertyTrait * @ORM\ManyToOne(targetEntity="Footprint") * @ORM\JoinColumn(name="id_footprint", referencedColumnName="id") * @Selectable() - * @Groups({"simple", "extended", "full"}) + * @Groups({"simple", "extended", "full", "import"}) */ protected ?Footprint $footprint = null; diff --git a/src/Entity/Parts/PartTraits/InstockTrait.php b/src/Entity/Parts/PartTraits/InstockTrait.php index 0abcc3da..6035fd40 100644 --- a/src/Entity/Parts/PartTraits/InstockTrait.php +++ b/src/Entity/Parts/PartTraits/InstockTrait.php @@ -56,7 +56,7 @@ trait InstockTrait * @var ?MeasurementUnit the unit in which the part's amount is measured * @ORM\ManyToOne(targetEntity="MeasurementUnit") * @ORM\JoinColumn(name="id_part_unit", referencedColumnName="id", nullable=true) - * @Groups({"extended", "full"}) + * @Groups({"extended", "full", "import"}) */ protected ?MeasurementUnit $partUnit = null; diff --git a/src/Entity/Parts/PartTraits/ManufacturerTrait.php b/src/Entity/Parts/PartTraits/ManufacturerTrait.php index 3e554f88..d87e11cc 100644 --- a/src/Entity/Parts/PartTraits/ManufacturerTrait.php +++ b/src/Entity/Parts/PartTraits/ManufacturerTrait.php @@ -39,7 +39,7 @@ trait ManufacturerTrait * @ORM\ManyToOne(targetEntity="Manufacturer") * @ORM\JoinColumn(name="id_manufacturer", referencedColumnName="id") * @Selectable() - * @Groups({"simple","extended", "full"}) + * @Groups({"simple","extended", "full", "import"}) */ protected ?Manufacturer $manufacturer = null; diff --git a/src/Repository/StructuralDBElementRepository.php b/src/Repository/StructuralDBElementRepository.php index e23eda8f..d7dae474 100644 --- a/src/Repository/StructuralDBElementRepository.php +++ b/src/Repository/StructuralDBElementRepository.php @@ -29,6 +29,12 @@ use RecursiveIteratorIterator; class StructuralDBElementRepository extends NamedDBElementRepository { + /** + * @var array An array containing all new entities created by getNewEntityByPath. + * This is used to prevent creating multiple entities for the same path. + */ + private array $new_entity_cache = []; + /** * Finds all nodes without a parent node. They are our root nodes. * @@ -91,7 +97,7 @@ class StructuralDBElementRepository extends NamedDBElementRepository } /** - * Creates a structure of AbsstractStructuralDBElements from a path separated by $separator, which splits the various levels. + * Creates a structure of AbstractStructuralDBElements from a path separated by $separator, which splits the various levels. * This function will try to use existing elements, if they are already in the database. If not, they will be created. * An array of the created elements will be returned, with the last element being the deepest element. * @param string $path @@ -108,14 +114,67 @@ class StructuralDBElementRepository extends NamedDBElementRepository continue; } - //See if we already have an element with this name and parent - $entity = $this->findOneBy(['name' => $name, 'parent' => $parent]); + //Use the cache to prevent creating multiple entities for the same path + $entity = $this->getNewEntityFromCache($name, $parent); + + //See if we already have an element with this name and parent in the database + if (!$entity) { + $entity = $this->findOneBy(['name' => $name, 'parent' => $parent]); + } if (null === $entity) { $class = $this->getClassName(); /** @var AbstractStructuralDBElement $entity */ $entity = new $class; $entity->setName($name); $entity->setParent($parent); + + $this->setNewEntityToCache($entity); + } + + $result[] = $entity; + $parent = $entity; + } + + return $result; + } + + private function getNewEntityFromCache(string $name, ?AbstractStructuralDBElement $parent): ?AbstractStructuralDBElement + { + $key = $parent ? $parent->getFullPath('%->%').'%->%'.$name : $name; + if (isset($this->new_entity_cache[$key])) { + return $this->new_entity_cache[$key]; + } + return null; + } + + private function setNewEntityToCache(AbstractStructuralDBElement $entity): void + { + $key = $entity->getFullPath('%->%'); + $this->new_entity_cache[$key] = $entity; + } + + /** + * Returns an element of AbstractStructuralDBElements queried from a path separated by $separator, which splits the various levels. + * An array of the created elements will be returned, with the last element being the deepest element. + * If no element was found, an empty array will be returned. + * @param string $path + * @param string $separator + * @return AbstractStructuralDBElement[] + */ + public function getEntityByPath(string $path, string $separator = '->'): array + { + $parent = null; + $result = []; + foreach (explode($separator, $path) as $name) { + $name = trim($name); + if ('' === $name) { + continue; + } + + //See if we already have an element with this name and parent + $entity = $this->findOneBy(['name' => $name, 'parent' => $parent]); + if (null === $entity) { + return []; } $result[] = $entity; diff --git a/src/Serializer/StructuralElementFromNameDenormalizer.php b/src/Serializer/StructuralElementFromNameDenormalizer.php new file mode 100644 index 00000000..e0b7a91b --- /dev/null +++ b/src/Serializer/StructuralElementFromNameDenormalizer.php @@ -0,0 +1,63 @@ +. + */ + +namespace App\Serializer; + +use App\Entity\Base\AbstractStructuralDBElement; +use App\Form\Type\StructuralEntityType; +use App\Repository\StructuralDBElementRepository; +use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; + +class StructuralElementFromNameDenormalizer implements ContextAwareDenormalizerInterface +{ + private EntityManagerInterface $em; + + public function __construct(EntityManagerInterface $em) + { + $this->em = $em; + } + + public function supportsDenormalization($data, string $type, string $format = null, array $context = []) + { + return is_string($data) && is_subclass_of($type, AbstractStructuralDBElement::class); + } + + public function denormalize($data, string $type, string $format = null, array $context = []) + { + //Retrieve the repository for the given type + /** @var StructuralDBElementRepository $repo */ + $repo = $this->em->getRepository($type); + + $path_delimiter = $context['path_delimiter'] ?? '->'; + + if ($context['create_unknown_datastructures'] ?? false) { + $elements = $repo->getNewEntityFromPath($data, $path_delimiter); + //Persist all new elements + foreach ($elements as $element) { + $this->em->persist($element); + } + return end($elements); + } + + $elements = $repo->getEntityByPath($data, $path_delimiter); + return end($elements); + } +} \ No newline at end of file diff --git a/src/Serializer/StructuralElementNormalizer.php b/src/Serializer/StructuralElementNormalizer.php index 684711da..ae880b93 100644 --- a/src/Serializer/StructuralElementNormalizer.php +++ b/src/Serializer/StructuralElementNormalizer.php @@ -21,6 +21,7 @@ namespace App\Serializer; use App\Entity\Base\AbstractStructuralDBElement; +use Symfony\Component\Serializer\Normalizer\ContextAwareDenormalizerInterface; use Symfony\Component\Serializer\Normalizer\ContextAwareNormalizerInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Serializer\Normalizer\ObjectNormalizer; @@ -48,7 +49,7 @@ class StructuralElementNormalizer implements ContextAwareNormalizerInterface $data = $this->normalizer->normalize($object, $format, $context); //Remove type field for CSV export - if ($format == 'csv') { + if ($format === 'csv') { unset($data['type']); } diff --git a/src/Services/ImportExportSystem/EntityImporter.php b/src/Services/ImportExportSystem/EntityImporter.php index 5097c04e..a6d82daa 100644 --- a/src/Services/ImportExportSystem/EntityImporter.php +++ b/src/Services/ImportExportSystem/EntityImporter.php @@ -160,6 +160,8 @@ class EntityImporter [ 'groups' => $groups, 'csv_delimiter' => $options['csv_delimiter'], + 'create_unknown_datastructures' => $options['create_unknown_datastructures'], + 'path_delimiter' => $options['path_delimiter'], ]); //Ensure we have an array of entity elements. @@ -215,7 +217,9 @@ class EntityImporter 'preserve_children' => true, 'parent' => null, //The parent element to which the imported elements should be added 'abort_on_validation_error' => true, - 'part_category' => null + 'part_category' => null, + 'create_unknown_datastructures' => true, //If true, unknown datastructures (categories, footprints, etc.) will be created on the fly + 'path_delimiter' => '->', //The delimiter used to separate the path elements in the name of a structural element ]); $resolver->setAllowedValues('format', ['csv', 'json', 'xml', 'yaml']); diff --git a/templates/parts/import/parts_import.html.twig b/templates/parts/import/parts_import.html.twig index 8d2c8961..bd9d8f09 100644 --- a/templates/parts/import/parts_import.html.twig +++ b/templates/parts/import/parts_import.html.twig @@ -12,7 +12,13 @@