Passed
Push — master ( 24e4a5...3438f1 )
by Jan
04:29
created

EntityImporter::fileToDBEntities()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 5
eloc 15
c 2
b 0
f 0
nc 6
nop 3
dl 0
loc 34
rs 9.4555
1
<?php
2
/**
3
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
4
 *
5
 * Copyright (C) 2019 Jan Böhmer (https://github.com/jbtronics)
6
 *
7
 * This program is free software; you can redistribute it and/or
8
 * modify it under the terms of the GNU General Public License
9
 * as published by the Free Software Foundation; either version 2
10
 * of the License, or (at your option) any later version.
11
 *
12
 * This program is distributed in the hope that it will be useful,
13
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15
 * GNU General Public License for more details.
16
 *
17
 * You should have received a copy of the GNU General Public License
18
 * along with this program; if not, write to the Free Software
19
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA
20
 */
21
22
namespace App\Services;
23
24
use App\Entity\Base\DBElement;
25
use App\Entity\Base\StructuralDBElement;
26
use Doctrine\ORM\EntityManagerInterface;
27
use Symfony\Component\HttpFoundation\File\File;
28
use Symfony\Component\OptionsResolver\OptionsResolver;
29
use Symfony\Component\Serializer\SerializerInterface;
30
use Symfony\Component\Validator\Validator\ValidatorInterface;
31
32
class EntityImporter
33
{
34
    protected $serializer;
35
    protected $em;
36
    protected $validator;
37
38
    public function __construct(SerializerInterface $serializer, EntityManagerInterface $em, ValidatorInterface $validator)
39
    {
40
        $this->serializer = $serializer;
41
        $this->em = $em;
42
        $this->validator = $validator;
43
    }
44
45
    protected function configureOptions(OptionsResolver $resolver)
46
    {
47
        $resolver->setDefaults([
48
            'csv_separator' => ';',
49
            'format' => 'json',
50
            'preserve_children' => true,
51
            'parent' => null,
52
            'abort_on_validation_error' => true,
53
        ]);
54
    }
55
56
    /**
57
     * Creates many entries at once, based on a (text) list of name.
58
     * The created enties are not persisted to database yet, so you have to do it yourself.
59
     *
60
     * @param string                   $lines      The list of names seperated by \n
61
     * @param string                   $class_name The name of the class for which the entities should be created
62
     * @param StructuralDBElement|null $parent     The element which will be used as parent element for new elements.
63
     * @param array                    $errors     An associative array containing all validation errors.
64
     *
65
     * @return StructuralDBElement[]  An array containing all valid imported entities (with the type $class_name)
66
     */
67
    public function massCreation(string $lines, string $class_name, ?StructuralDBElement $parent = null, array &$errors = []): array
68
    {
69
        //Expand every line to a single entry:
70
        $names = explode("\n", $lines);
71
72
        if (!is_a($class_name, StructuralDBElement::class, true)) {
73
            throw new \InvalidArgumentException('$class_name must be a StructuralDBElement type!');
74
        }
75
        if ($parent !== null && !is_a($parent, $class_name)) {
76
            throw new \InvalidArgumentException('$parent must have the same type as specified in $class_name!');
77
        }
78
79
        $errors = [];
80
        $valid_entities = [];
81
82
        foreach ($names as $name) {
83
            $name = trim($name);
84
            if ($name === '') {
85
                //Skip empty lines (StrucuralDBElements must have a name)
86
                continue;
87
            }
88
            /** @var $entity StructuralDBElement */
89
            //Create new element with given name
90
            $entity = new $class_name();
91
            $entity->setName($name);
92
            $entity->setParent($parent);
93
94
            //Validate entity
95
            $tmp = $this->validator->validate($entity);
96
            //If no error occured, write entry to DB:
97
            if (0 === \count($tmp)) {
98
                $valid_entities[] = $entity;
99
            } else { //Otherwise log error
100
                $errors[] = ['entity' => $entity, 'violations' => $tmp];
101
            }
102
        }
103
104
        return $valid_entities;
105
    }
106
107
    /**
108
     * This methods deserializes the given file and saves it database.
109
     * The imported elements will be checked (validated) before written to database.
110
     *
111
     * @param File   $file       The file that should be used for importing.
112
     * @param string $class_name The class name of the enitity that should be imported.
113
     * @param array  $options    Options for the import process.
114
     *
115
     * @return array An associative array containing an ConstraintViolationList and the entity name as key are returned,
116
     *               if an error happened during validation. When everything was successfull, the array should be empty.
117
     */
118
    public function fileToDBEntities(File $file, string $class_name, array $options = []): array
119
    {
120
        $resolver = new OptionsResolver();
121
        $this->configureOptions($resolver);
122
123
        $options = $resolver->resolve($options);
124
125
        $entities = $this->fileToEntityArray($file, $class_name, $options);
126
127
        $errors = [];
128
129
        //Iterate over each $entity write it to DB.
130
        foreach ($entities as $entity) {
131
            /* @var StructuralDBElement $entity */
132
            //Move every imported entity to the selected parent
133
            $entity->setParent($options['parent']);
134
135
            //Validate entity
136
            $tmp = $this->validator->validate($entity);
137
138
            //When no validation error occured, persist entity to database (cascade must be set in entity)
139
            if (0 === \count($errors)) {
140
                $this->em->persist($entity);
141
            } else { //Log validation errors to global log.
142
                $errors[$entity->getFullPath()] = $tmp;
143
            }
144
        }
145
146
        //Save changes to database, when no error happened, or we should continue on error.
147
        if (empty($errors) || false == $options['abort_on_validation_error']) {
148
            $this->em->flush();
149
        }
150
151
        return $errors;
152
    }
153
154
    /**
155
     * This method converts (deserialize) a (uploaded) file to an array of entities with the given class.
156
     *
157
     * The imported elements will NOT be validated. If you want to use the result array, you have to validate it by yourself.
158
     *
159
     * @param File   $file       The file that should be used for importing.
160
     * @param string $class_name The class name of the enitity that should be imported.
161
     * @param array  $options    Options for the import process.
162
     *
163
     * @return array An array containing the deserialized elements.
164
     */
165
    public function fileToEntityArray(File $file, string $class_name, array $options = []): array
166
    {
167
        $resolver = new OptionsResolver();
168
        $this->configureOptions($resolver);
169
170
        $options = $resolver->resolve($options);
171
172
        //Read file contents
173
        $content = file_get_contents($file->getRealPath());
174
175
        $groups = ['simple'];
176
        //Add group when the children should be preserved
177
        if ($options['preserve_children']) {
178
            $groups[] = 'include_children';
179
        }
180
181
        //The [] behind class_name denotes that we expect an array.
182
        $entities = $this->serializer->deserialize($content, $class_name.'[]', $options['format'],
183
            ['groups' => $groups, 'csv_delimiter' => $options['csv_separator']]);
184
185
        //Ensure we have an array of entitity elements.
186
        if (!\is_array($entities)) {
187
            $entities = [$entities];
188
        }
189
190
        //The serializer has only set the children attributes. We also have to change the parent value (the real value in DB)
191
        if ($entities[0] instanceof StructuralDBElement) {
192
            $this->correctParentEntites($entities, null);
193
        }
194
195
        return $entities;
196
    }
197
198
    /**
199
     * This functions corrects the parent setting based on the children value of the parent.
200
     *
201
     * @param iterable $entities The list of entities that should be fixed.
202
     * @param null     $parent   The parent, to which the entity should be set.
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $parent is correct as it would always require null to be passed?
Loading history...
203
     */
204
    protected function correctParentEntites(iterable $entities, $parent = null)
205
    {
206
        foreach ($entities as $entity) {
207
            /* @var $entity StructuralDBElement */
208
            $entity->setParent($parent);
209
            //Do the same for the children of entity
210
            $this->correctParentEntites($entity->getChildren(), $entity);
211
        }
212
    }
213
}
214