Completed
Push — main ( d7fef9...7c1c2b )
by Dante
47s queued 43s
created

Import::saveObject()   B

Complexity

Conditions 8
Paths 16

Size

Total Lines 36
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 24
c 1
b 0
f 0
dl 0
loc 36
rs 8.4444
cc 8
nc 16
nop 1
1
<?php
2
declare(strict_types=1);
3
4
/**
5
 * BEdita, API-first content management framework
6
 * Copyright 2023 Atlas Srl, Chialab Srl
7
 *
8
 * This file is part of BEdita: you can redistribute it and/or modify
9
 * it under the terms of the GNU Lesser General Public License as published
10
 * by the Free Software Foundation, either version 3 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * See LICENSE.LGPL or <http://gnu.org/licenses/lgpl-3.0.html> for more details.
14
 */
15
16
namespace BEdita\ImportTools\Utility;
17
18
use BEdita\Core\Model\Entity\ObjectEntity;
19
use BEdita\Core\Model\Entity\Translation;
20
use Cake\Http\Exception\BadRequestException;
21
use Cake\Log\LogTrait;
22
use Cake\ORM\Locator\LocatorAwareTrait;
23
use Cake\Utility\Hash;
24
25
class Import
26
{
27
    use CsvTrait;
28
    use LocatorAwareTrait;
29
    use LogTrait;
30
    use TreeTrait;
31
32
    /**
33
     * @inheritDoc
34
     */
35
    protected $_defaultConfig = [
36
        'defaults' => [
37
            'status' => 'on',
38
        ],
39
        'csv' => [
40
            'delimiter' => ',',
41
            'enclosure' => '"',
42
            'escape' => '"',
43
        ],
44
    ];
45
46
    /**
47
     * Dry run mode flag
48
     *
49
     * @var bool
50
     */
51
    public bool $dryrun = false;
52
53
    /**
54
     * Full filename path
55
     *
56
     * @var string|null
57
     */
58
    public ?string $filename = '';
59
60
    /**
61
     * Parent uname or ID
62
     *
63
     * @var string|null
64
     */
65
    public ?string $parent = '';
66
67
    /**
68
     * Number of processed entities
69
     *
70
     * @var int
71
     */
72
    public int $processed = 0;
73
74
    /**
75
     * Number of saved entities
76
     *
77
     * @var int
78
     */
79
    public int $saved = 0;
80
81
    /**
82
     * Number of errors
83
     *
84
     * @var int
85
     */
86
    public int $errors = 0;
87
88
    /**
89
     * Errors details
90
     *
91
     * @var array
92
     */
93
    public array $errorsDetails = [];
94
95
    /**
96
     * Number of skipped
97
     *
98
     * @var int
99
     */
100
    public int $skipped = 0;
101
102
    /**
103
     * Entity type
104
     *
105
     * @var string
106
     */
107
    public string $type = '';
108
109
    /**
110
     * Objects table
111
     *
112
     * @var \BEdita\Core\Model\Table\ObjectsTable
113
     */
114
    protected $objectsTable;
115
116
    /**
117
     * Type table
118
     *
119
     * @var \BEdita\Core\Model\Table\ObjectsTable
120
     */
121
    protected $typeTable;
122
123
    /**
124
     * Translations table
125
     *
126
     * @var \BEdita\Core\Model\Table\TranslationsTable
127
     */
128
    protected $translationsTable;
129
130
    /**
131
     * Constructor
132
     *
133
     * @param string|null $filename Full filename path
134
     * @param string|null $type Entity type
135
     * @param string|null $parent Parent uname or ID
136
     * @param bool|null $dryrun Dry run mode flag
137
     * @return void
138
     */
139
    public function __construct(
140
        ?string $filename = null,
141
        ?string $type = 'objects',
142
        ?string $parent = null,
143
        ?bool $dryrun = false
144
    ) {
145
        $this->filename = $filename;
146
        $this->type = $type;
147
        $this->parent = $parent;
148
        $this->dryrun = $dryrun;
149
        $this->processed = 0;
150
        $this->saved = 0;
151
        $this->errors = 0;
152
        $this->skipped = 0;
153
        $this->errorsDetails = [];
154
        /** @var \BEdita\Core\Model\Table\ObjectsTable $objectsTable */
155
        $objectsTable = $this->fetchTable('objects');
156
        $this->objectsTable = $objectsTable;
157
        /** @var \BEdita\Core\Model\Table\ObjectsTable $typesTable */
158
        $typesTable = $this->fetchTable($this->type);
159
        $this->typeTable = $typesTable;
160
        /** @var \BEdita\Core\Model\Table\TranslationsTable $translationsTable */
161
        $translationsTable = $this->fetchTable('translations');
162
        $this->translationsTable = $translationsTable;
163
    }
164
165
    /**
166
     * Save objects
167
     *
168
     * @return void
169
     */
170
    public function saveObjects(): void
171
    {
172
        foreach ($this->readCsv($this->filename) as $obj) {
173
            try {
174
                $this->saveObject($obj);
175
            } catch (\Exception $e) {
176
                $this->errorsDetails[] = $e->getMessage();
177
                $this->errors++;
178
            } finally {
179
                $this->processed++;
180
            }
181
        }
182
    }
183
184
    /**
185
     * Save object
186
     *
187
     * @param array $obj Object data
188
     * @return \BEdita\Core\Model\Entity\ObjectEntity
189
     */
190
    public function saveObject(array $obj): ObjectEntity
191
    {
192
        $entity = $this->typeTable->newEmptyEntity();
193
        if (!empty($obj['uname']) || !empty($obj['id'])) {
194
            $uname = (string)Hash::get($obj, 'uname');
195
            $identifier = empty($uname) ? 'id' : 'uname';
196
            $conditions = [$identifier => (string)Hash::get($obj, $identifier)];
197
            if ($this->objectsTable->exists($conditions)) {
198
                /** @var \BEdita\Core\Model\Entity\ObjectEntity $o */
199
                $o = $this->objectsTable->find()->where($conditions)->firstOrFail();
200
                if ($o->type !== $this->type) {
201
                    throw new BadRequestException(
202
                        sprintf(
203
                            'Object "%s" already present with another type "%s"',
204
                            $conditions[$identifier],
205
                            $o->type
206
                        )
207
                    );
208
                }
209
                $entity = $o->getTable()->find('type', [$this->type])->where($conditions)->firstOrFail();
210
            }
211
        }
212
        $entity = $this->typeTable->patchEntity($entity, $obj);
213
        $entity->set('type', $this->type);
214
        if ($this->dryrun === true) {
215
            $this->skipped++;
216
217
            return $entity;
218
        }
219
        $this->typeTable->saveOrFail($entity);
220
        if (!empty($this->parent)) {
221
            $this->setParent($entity, $this->parent);
222
        }
223
        $this->saved++;
224
225
        return $entity;
226
    }
227
228
    /**
229
     * Save translations
230
     *
231
     * @return void
232
     */
233
    public function saveTranslations(): void
234
    {
235
        foreach ($this->readCsv($this->filename) as $translation) {
236
            try {
237
                $this->saveTranslation($translation);
238
            } catch (\Exception $e) {
239
                $this->errorsDetails[] = $e->getMessage();
240
                $this->errors++;
241
            } finally {
242
                $this->processed++;
243
            }
244
        }
245
    }
246
247
    /**
248
     * Save translation
249
     *
250
     * @param array $data Translation data
251
     * @return \BEdita\Core\Model\Entity\Translation
252
     * @throws \Cake\Http\Exception\BadRequestException
253
     */
254
    public function saveTranslation(array $data): Translation
255
    {
256
        $uname = (string)Hash::get($data, 'object_uname');
257
        if (!$this->objectsTable->exists(compact('uname'))) {
258
            throw new BadRequestException(sprintf('Object "%s" not found', $uname));
259
        }
260
        /** @var \BEdita\Core\Model\Entity\ObjectEntity $o */
261
        $o = $this->objectsTable->find()->where(compact('uname'))->firstOrFail();
262
        $objectId = $o->id;
263
        /** @var \BEdita\Core\Model\Entity\Translation $entity */
264
        $entity = $this->translationsTable->find()
265
            ->where([
266
                'object_id' => $objectId,
267
                'lang' => $data['lang'],
268
            ])
269
            ->first();
270
        $translation = [
271
            'object_id' => $objectId,
272
        ];
273
        if ($entity != null) {
274
            $entity = $this->translationsTable->patchEntity($entity, $translation);
275
        } else {
276
            $entity = $this->translationsTable->newEntity($translation);
277
        }
278
        $entity->set('translated_fields', $this->translatedFields($data));
279
        $entity->set('status', $this->getConfig('defaults')['status']);
280
        $entity->set('lang', $data['lang']);
281
        if ($this->dryrun === true) {
282
            $this->skipped++;
283
284
            return $entity;
285
        }
286
        $this->translationsTable->saveOrFail($entity);
287
        $this->saved++;
288
289
        return $entity;
290
    }
291
292
    /**
293
     * Get translated fields
294
     *
295
     * @param array $source Source data
296
     * @return array
297
     */
298
    public function translatedFields(array $source): array
299
    {
300
        $fields = (string)Hash::get($source, 'translated_fields');
301
        if (!empty($fields)) {
302
            return json_decode($fields, true);
303
        }
304
        $fields = [];
305
        foreach ($source as $key => $value) {
306
            if (in_array($key, ['id', 'object_uname', 'lang'])) {
307
                continue;
308
            }
309
            $subkey = strpos($key, 'translation_') === 0 ? substr($key, 12) : $key;
310
            $fields[$subkey] = $value;
311
        }
312
313
        return $fields;
314
    }
315
}
316