Passed
Pull Request — main (#4)
by Dante
12:41
created

ImportCommand::translationFields()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 10
c 1
b 0
f 0
dl 0
loc 13
rs 9.9332
cc 2
nc 2
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\Command;
17
18
use BEdita\Core\Model\Table\RolesTable;
19
use BEdita\Core\Model\Table\UsersTable;
20
use BEdita\Core\Utility\LoggedUser;
21
use BEdita\ImportTools\Utility\CsvTrait;
22
use BEdita\ImportTools\Utility\TreeTrait;
23
use Cake\Command\Command;
24
use Cake\Console\Arguments;
25
use Cake\Console\ConsoleIo;
26
use Cake\Console\ConsoleOptionParser;
27
use Cake\Http\Exception\BadRequestException;
28
use Cake\Utility\Hash;
29
30
/**
31
 * Import command.
32
 *
33
 * $ bin/cake import --help
34
 *
35
 * Usage:
36
 * cake import [options]
37
 *
38
 * Options:
39
 *
40
 * --dryrun, -d   dry run mode
41
 * --file, -f     CSV file to import (required)
42
 * --help, -h     Display this help.
43
 * --parent, -p   destination folder uname
44
 * --quiet, -q    Enable quiet output.
45
 * --type, -t     entity type to import (required)
46
 * --verbose, -v  Enable verbose output.
47
 *
48
 * # basic
49
 * $ bin/cake import --file documents.csv --type documents
50
 * $ bin/cake import -f documents.csv -t documents
51
 *
52
 * # dry-run
53
 * $ bin/cake import --file articles.csv --type articles --dryrun yes
54
 * $ bin/cake import -f articles.csv -t articles -d yes
55
 *
56
 * # destination folder
57
 * $ bin/cake import --file news.csv --type news --parent my-folder-uname
58
 * $ bin/cake import -f news.csv -t news -p my-folder-uname
59
 *
60
 * # translations
61
 * $ bin/cake import --file translations.csv --type translations
62
 * $ bin/cake import -f translations.csv -t translations
63
 */
64
class ImportCommand extends Command
65
{
66
    use CsvTrait;
67
    use TreeTrait;
68
69
    /**
70
     * @inheritDoc
71
     */
72
    protected $_defaultConfig = [
73
        'defaults' => [
74
            'status' => 'on',
75
        ],
76
        'csv' => [
77
            'delimiter' => ',',
78
            'enclosure' => '"',
79
            'escape' => '"',
80
        ],
81
    ];
82
83
    /**
84
     * Dry run mode flag
85
     *
86
     * @var bool
87
     */
88
    protected bool $dryrun = false;
89
90
    /**
91
     * Full filename path
92
     *
93
     * @var string|null
94
     */
95
    protected ?string $filename = '';
96
97
    /**
98
     * Parent uname or ID
99
     *
100
     * @var string|null
101
     */
102
    protected ?string $parent = '';
103
104
    /**
105
     * Number of processed entities
106
     *
107
     * @var int
108
     */
109
    protected int $processed = 0;
110
111
    /**
112
     * Number of saved entities
113
     *
114
     * @var int
115
     */
116
    protected int $saved = 0;
117
118
    /**
119
     * Entity type
120
     *
121
     * @var string
122
     */
123
    protected string $type = '';
124
125
    /**
126
     * @inheritDoc
127
     */
128
    public function buildOptionParser(ConsoleOptionParser $parser): ConsoleOptionParser
129
    {
130
        $parser = parent::buildOptionParser($parser);
131
        $parser->addOption('file', [
132
                'help' => 'CSV file to import',
133
                'required' => true,
134
                'short' => 'f',
135
            ])
136
            ->addOption('type', [
137
                'help' => 'entity type to import',
138
                'required' => true,
139
                'short' => 't',
140
            ])
141
            ->addOption('parent', [
142
                'help' => 'destination folder uname',
143
                'required' => false,
144
                'short' => 'p',
145
            ])
146
            ->addOption('dryrun', [
147
                'help' => 'dry run mode',
148
                'required' => false,
149
                'short' => 'd',
150
            ]);
151
152
        return $parser;
153
    }
154
155
    /**
156
     * @inheritDoc
157
     */
158
    public function execute(Arguments $args, ConsoleIo $io)
159
    {
160
        $this->filename = $args->getOption('file');
161
        if (!file_exists($this->filename)) {
162
            $io->out(sprintf('Bad csv source file name "%s"', $this->filename));
163
164
            return;
165
        }
166
        $this->type = $args->getOption('type');
167
        $this->parent = $args->getOption('parent');
168
        if ($args->getOption('dryrun')) {
169
            $this->dryrun = true;
170
        }
171
        $io->out('---------------------------------------');
172
        $io->out('Start');
173
        $io->out(sprintf('File: %s', $this->filename));
174
        $io->out(sprintf('Type: %s', $this->type));
175
        $io->out(sprintf('Parent: %s', empty($this->parent) ? 'none' : $this->parent));
176
        $io->out(sprintf('Dry run mode: %s', $this->dryrun === true ? 'yes' : 'no'));
177
        LoggedUser::setUser(['id' => UsersTable::ADMIN_USER, 'roles' => [['id' => RolesTable::ADMIN_ROLE]]]);
178
        $method = $this->type !== 'translations' ? 'objects' : 'translations';
179
        $this->$method();
180
        $io->out(sprintf('Processed: %d, Saved: %d', $this->processed, $this->saved));
181
        $io->out('Done, bye!');
182
        $io->out('---------------------------------------');
183
    }
184
185
    /**
186
     * Save objects
187
     *
188
     * @return void
189
     */
190
    protected function objects(): void
191
    {
192
        $objectsTable = $this->fetchTable('objects');
193
        $table = $this->fetchTable($this->type);
194
        foreach ($this->readCsv($this->filename) as $obj) {
195
            $this->processed++;
196
            $entity = $table->newEmptyEntity();
197
            if (!empty($obj['uname'])) {
198
                $uname = $obj['uname'];
199
                if ($objectsTable->exists(compact('uname'))) {
200
                    /** @var \BEdita\Core\Model\Entity\ObjectEntity $o */
201
                    $o = $objectsTable->find()->where(compact('uname'))->firstOrFail();
202
                    if ($o->type !== $this->type) {
203
                        throw new BadRequestException(
204
                            sprintf('Object uname "%s" already present with another type "%s"', $uname, $o->type)
205
                        );
206
                    }
207
                    $entity = $table->get($table->getId($uname));
208
                }
209
            }
210
            if ($this->dryrun === true) {
211
                continue;
212
            }
213
            $entity = $table->patchEntity($entity, $obj);
214
            $entity->set('type', $this->type);
215
            $table->saveOrFail($entity);
216
            if (isset($this->parent)) {
217
                $this->setParent($entity, $this->parent);
218
            }
219
            $this->saved++;
220
        }
221
    }
222
223
    /**
224
     * Save translations
225
     *
226
     * @return void
227
     */
228
    protected function translations(): void
229
    {
230
        foreach ($this->readCsv($this->filename) as $translation) {
231
            $this->processed++;
232
            $this->translationFields($translation);
233
            if ($this->dryrun === true) {
234
                continue;
235
            }
236
            $this->saveTranslation($translation);
237
            $this->saved++;
238
        }
239
    }
240
241
    /**
242
     * Setup translations fields
243
     *
244
     * @param array $translation Translation data
245
     * @return void
246
     */
247
    protected function translationFields(array &$translation): void
248
    {
249
        $objectUname = (string)Hash::get($translation, 'object_uname');
250
        $objectEntity = $this->fetchTable('Objects')->find('unameId', [$objectUname])->firstOrFail();
251
        $entity = $this->fetchTable('Translations')->find()->where([
252
            'object_id' => $objectEntity->id,
253
            'lang' => $translation['lang'],
254
        ])->first();
255
        if ($entity) {
256
            $translation['id'] = $entity->id;
257
        }
258
        unset($translation['object_uname']);
259
        $translation['object_id'] = $objectEntity->id;
260
    }
261
262
    /**
263
     * Save translation
264
     *
265
     * @param array $translation Translation data
266
     * @return void
267
     */
268
    protected function saveTranslation(array &$translation): void
269
    {
270
        $table = $this->fetchTable('Translations');
271
        $entity = $table->newEntity($translation);
272
        $entity->set('translated_fields', json_decode($translation['translated_fields'], true));
273
        $entity->set('status', $this->getConfig('defaults')['status']);
274
        if (!empty($translation['id'])) {
275
            $entity->set('id', $translation['id']);
276
        }
277
        $table->saveOrFail($entity);
278
    }
279
}
280