diff --git a/src/Controller/AttachmentTypeController.php b/src/Controller/AttachmentTypeController.php index 55bdfd17..f8952259 100644 --- a/src/Controller/AttachmentTypeController.php +++ b/src/Controller/AttachmentTypeController.php @@ -37,14 +37,18 @@ use App\Entity\NamedDBElement; use App\Entity\StructuralDBElement; use App\Form\BaseEntityAdminForm; use App\Form\ExportType; +use App\Form\ImportType; use App\Services\EntityExporter; +use App\Services\EntityImporter; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\ResponseHeaderBag; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Serializer\SerializerInterface; +use Symfony\Component\Validator\ConstraintViolationList; /** * @Route("/attachment_type") @@ -80,12 +84,13 @@ class AttachmentTypeController extends AbstractController * * @return \Symfony\Component\HttpFoundation\Response */ - public function new(Request $request, EntityManagerInterface $em) + public function new(Request $request, EntityManagerInterface $em, EntityImporter $importer) { $new_entity = new AttachmentType(); $this->denyAccessUnlessGranted('create', $new_entity); + //Basic edit form $form = $this->createForm(BaseEntityAdminForm::class, $new_entity); $form->handleRequest($request); @@ -98,9 +103,30 @@ class AttachmentTypeController extends AbstractController return $this->redirectToRoute('attachment_type_edit', ['id' => $new_entity->getID()]); } + //Import form + $import_form = $this->createForm(ImportType::class, ['entity_class' => AttachmentType::class]); + $import_form->handleRequest($request); + + if ($import_form->isSubmitted() && $import_form->isValid()) { + /** @var UploadedFile $file */ + $file = $import_form['file']->getData(); + $data = $import_form->getData(); + + $options = array('parent' => $data['parent'], 'preserve_children' => $data['preserve_children'], + 'format' => $data['format'], 'csv_separator' => $data['csv_separator']); + + $errors = $importer->fileToDBEntities($file, AttachmentType::class, $options); + + foreach ($errors as $name => $error) { + /** @var $error ConstraintViolationList */ + $this->addFlash('error', $name . ":" . $error); + } + } + return $this->render('AdminPages/AttachmentTypeAdmin.html.twig', [ 'entity' => $new_entity, - 'form' => $form->createView() + 'form' => $form->createView(), + 'import_form' => $import_form->createView() ]); } diff --git a/src/Entity/AttachmentType.php b/src/Entity/AttachmentType.php index 9f3a3f0f..0e5c4311 100644 --- a/src/Entity/AttachmentType.php +++ b/src/Entity/AttachmentType.php @@ -43,7 +43,7 @@ class AttachmentType extends StructuralDBElement protected $attachments; /** - * @ORM\OneToMany(targetEntity="AttachmentType", mappedBy="parent") + * @ORM\OneToMany(targetEntity="AttachmentType", mappedBy="parent", cascade={"persist"}) */ protected $children; diff --git a/src/Entity/StructuralDBElement.php b/src/Entity/StructuralDBElement.php index ae40a8d5..d60f1687 100644 --- a/src/Entity/StructuralDBElement.php +++ b/src/Entity/StructuralDBElement.php @@ -23,6 +23,7 @@ declare(strict_types=1); namespace App\Entity; +use Doctrine\Common\Collections\ArrayCollection; use Doctrine\ORM\Mapping as ORM; use Doctrine\ORM\PersistentCollection; use App\Validator\Constraints\NoneOfItsChildren; @@ -88,6 +89,13 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement */ private $full_path_strings; + + + public function __construct() + { + $this->children = new ArrayCollection(); + } + /****************************************************************************** * StructuralDBElement constructor. *****************************************************************************/ @@ -220,12 +228,12 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement * * @return static[] all subelements as an array of objects (sorted by their full path) */ - public function getSubelements(): PersistentCollection + public function getSubelements(): iterable { return $this->children; } - public function getChildren(): PersistentCollection + public function getChildren(): iterable { return $this->children; } @@ -264,4 +272,18 @@ abstract class StructuralDBElement extends AttachmentContainingDBElement return $this; } + + public function setChildren(array $element) : self + { + $this->children = $element; + + return $this; + } + + public function clearChildren() : self + { + $this->children = new ArrayCollection(); + + return $this; + } } diff --git a/src/Form/ImportType.php b/src/Form/ImportType.php new file mode 100644 index 00000000..15e66aa8 --- /dev/null +++ b/src/Form/ImportType.php @@ -0,0 +1,65 @@ +add('format', ChoiceType::class, ['choices' => + ['JSON' => 'json', 'XML' => 'xml','CSV'=>'csv' ,'YAML' => 'yaml']]) + ->add('csv_separator', TextType::class, ['data' => ';']) + ->add('parent', EntityType::class, ['class' => $data['entity_class'], 'choice_label' => 'full_path', + 'attr' => ['class' => 'selectpicker', 'data-live-search' => true], 'required' => false, 'label' => 'parent.label', + ]) + ->add('preserve_children', CheckboxType::class, ['data' => true, 'required' => false]) + ->add('file', FileType::class) + + //Buttons + ->add('import', SubmitType::class, ['label' => 'import.btn']); + } +} \ No newline at end of file diff --git a/src/Services/EntityExporter.php b/src/Services/EntityExporter.php index f2380b28..13d30434 100644 --- a/src/Services/EntityExporter.php +++ b/src/Services/EntityExporter.php @@ -96,7 +96,14 @@ class EntityExporter break; } - $response = new Response($this->serializer->serialize($entity, $format, + //Ensure that we always serialize an array. This makes it easier to import the data again. + if(is_array($entity)) { + $entity_array = $entity; + } else { + $entity_array = [$entity]; + } + + $response = new Response($this->serializer->serialize($entity_array, $format, [ 'groups' => $groups, 'as_collection' => true, diff --git a/src/Services/EntityImporter.php b/src/Services/EntityImporter.php new file mode 100644 index 00000000..7772bee7 --- /dev/null +++ b/src/Services/EntityImporter.php @@ -0,0 +1,169 @@ +serializer = $serializer; + $this->em = $em; + $this->validator = $validator; + } + + protected function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'csv_separator' => ';', + 'format' => 'json', + 'preserve_children' => true, + 'parent' => null, + 'abort_on_validation_error' => true + ]); + } + + /** + * This methods deserializes the given file and saves it database. + * The imported elements will be checked (validated) before written to database. + * @param File $file The file that should be used for importing. + * @param string $class_name The class name of the enitity that should be imported. + * @param array $options Options for the import process. + * @return array An associative array containing an ConstraintViolationList and the entity name as key are returned, + * if an error happened during validation. When everything was successfull, the array should be empty. + */ + public function fileToDBEntities(File $file, string $class_name, array $options = []) : array + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + + $options = $resolver->resolve($options); + + + $entities = $this->fileToEntityArray($file, $class_name, $options); + + $errors = array(); + + //Iterate over each $entity write it to DB. + foreach ($entities as $entity) { + /** @var StructuralDBElement $entity */ + //Move every imported entity to the selected parent + $entity->setParent($options['parent']); + + //Validate entity + $tmp = $this->validator->validate($entity); + + //When no validation error occured, persist entity to database (cascade must be set in entity) + if ($tmp === null) { + $this->em->persist($entity); + } else { //Log validation errors to global log. + $errors[$entity->getFullPath()] = $tmp; + } + } + + //Save changes to database, when no error happened, or we should continue on error. + if (empty($errors) || $options['abort_on_validation_error'] == false) { + $this->em->flush(); + } + + return $errors; + } + + /** + * This method converts (deserialize) a (uploaded) file to an array of entities with the given class. + * + * The imported elements will NOT be validated. If you want to use the result array, you have to validate it by yourself. + * @param File $file The file that should be used for importing. + * @param string $class_name The class name of the enitity that should be imported. + * @param array $options Options for the import process. + * @return array An array containing the deserialized elements. + */ + public function fileToEntityArray(File $file, string $class_name, array $options = []) : array + { + $resolver = new OptionsResolver(); + $this->configureOptions($resolver); + + $options = $resolver->resolve($options); + + //Read file contents + $content = file_get_contents($file->getRealPath()); + + $groups = ['simple']; + //Add group when the children should be preserved + if ($options['preserve_children']) { + $groups[] = 'include_children'; + } + + //The [] behind class_name denotes that we expect an array. + $entities = $this->serializer->deserialize($content, $class_name . '[]', $options['format'], ['groups' => $groups]); + + //Ensure we have an array of entitity elements. + if(!is_array($entities)) { + $entities = [$entities]; + } + + //The serializer has only set the children attributes. We also have to change the parent value (the real value in DB) + if ($entities[0] instanceof StructuralDBElement) { + $this->correctParentEntites($entities, null); + } + + return $entities; + } + + /** + * This functions corrects the parent setting based on the children value of the parent. + * @param iterable $entities The list of entities that should be fixed. + * @param null $parent The parent, to which the entity should be set. + */ + protected function correctParentEntites(iterable $entities, $parent = null) + { + foreach ($entities as $entity) { + /** @var $entity StructuralDBElement */ + $entity->setParent($parent); + //Do the same for the children of entity + $this->correctParentEntites($entity->getChildren(), $entity); + } + } +} \ No newline at end of file diff --git a/templates/AdminPages/EntityAdminBase.html.twig b/templates/AdminPages/EntityAdminBase.html.twig index df91d72b..dae0dcbb 100644 --- a/templates/AdminPages/EntityAdminBase.html.twig +++ b/templates/AdminPages/EntityAdminBase.html.twig @@ -148,7 +148,9 @@ {% else %} {# For new element we have a combined import/export tab #}