Passed
Pull Request — main (#4)
by Dante
01:14
created

ImportCommand   A

Complexity

Total Complexity 21

Size/Duplication

Total Lines 223
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 21
eloc 102
c 1
b 0
f 0
dl 0
loc 223
rs 10

6 Methods

Rating   Name   Duplication   Size   Complexity  
A buildOptionParser() 0 25 1
B execute() 0 25 6
B objects() 0 32 7
A translations() 0 10 3
A translationFields() 0 20 2
A saveTranslation() 0 10 2
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
        /** @var \BEdita\Core\Model\Table\ObjectsTable $objectsTable */
193
        $objectsTable = $this->fetchTable('objects');
194
        /** @var \BEdita\Core\Model\Table\ObjectsTable $table */
195
        $table = $this->fetchTable($this->type);
196
        foreach ($this->readCsv($this->filename) as $obj) {
197
            $this->processed++;
198
            $entity = $table->newEmptyEntity();
199
            if (!empty($obj['uname'])) {
200
                $uname = $obj['uname'];
201
                if ($objectsTable->exists(compact('uname'))) {
202
                    /** @var \BEdita\Core\Model\Entity\ObjectEntity $o */
203
                    $o = $objectsTable->find()->where(compact('uname'))->firstOrFail();
204
                    if ($o->type !== $this->type) {
205
                        throw new BadRequestException(
206
                            sprintf('Object uname "%s" already present with another type "%s"', $uname, $o->type)
207
                        );
208
                    }
209
                    $entity = $table->get($table->getId($uname));
210
                }
211
            }
212
            if ($this->dryrun === true) {
213
                continue;
214
            }
215
            $entity = $table->patchEntity($entity, $obj);
216
            $entity->set('type', $this->type);
217
            $table->saveOrFail($entity);
218
            if (isset($this->parent)) {
219
                $this->setParent($entity, $this->parent);
220
            }
221
            $this->saved++;
222
        }
223
    }
224
225
    /**
226
     * Save translations
227
     *
228
     * @return void
229
     */
230
    protected function translations(): void
231
    {
232
        foreach ($this->readCsv($this->filename) as $translation) {
233
            $this->processed++;
234
            $this->translationFields($translation);
235
            if ($this->dryrun === true) {
236
                continue;
237
            }
238
            $this->saveTranslation($translation);
239
            $this->saved++;
240
        }
241
    }
242
243
    /**
244
     * Setup translations fields
245
     *
246
     * @param array $translation Translation data
247
     * @return void
248
     */
249
    protected function translationFields(array &$translation): void
250
    {
251
        $objectUname = (string)Hash::get($translation, 'object_uname');
252
        /** @var \BEdita\Core\Model\Table\ObjectsTable $objectsTable */
253
        $objectsTable = $this->fetchTable('Objects');
254
        $objectId = $objectsTable->getId($objectUname);
255
        /** @var \BEdita\Core\Model\Table\TranslationsTable $translationsTable */
256
        $translationsTable = $this->fetchTable('Translations');
257
        /** @var \BEdita\Core\Model\Entity\Translation $entity */
258
        $entity = $translationsTable->find()
259
            ->where([
260
                'object_id' => $objectId,
261
                'lang' => $translation['lang'],
262
            ])
263
            ->first();
264
        if ($entity->id) {
265
            $translation['id'] = $entity->id;
266
        }
267
        unset($translation['object_uname']);
268
        $translation['object_id'] = $objectId;
269
    }
270
271
    /**
272
     * Save translation
273
     *
274
     * @param array $translation Translation data
275
     * @return void
276
     */
277
    protected function saveTranslation(array &$translation): void
278
    {
279
        $table = $this->fetchTable('Translations');
280
        $entity = $table->newEntity($translation);
281
        $entity->set('translated_fields', json_decode($translation['translated_fields'], true));
282
        $entity->set('status', $this->getConfig('defaults')['status']);
283
        if (!empty($translation['id'])) {
284
            $entity->set('id', $translation['id']);
285
        }
286
        $table->saveOrFail($entity);
287
    }
288
}
289