Passed
Branch beta (1b8e35)
by Jon
07:16
created

DataImporter   D

Complexity

Total Complexity 59

Size/Duplication

Total Lines 474
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 59
dl 0
loc 474
rs 4.5454
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
C getChangeset() 0 83 9
C getColumnProfileMap() 0 61 11
D __construct() 0 43 10
A makeErrorMessage() 0 3 1
A getHeaderFromMap() 0 3 1
A getAddRows() 0 6 1
A validateRequired() 0 8 3
B makeFqn() 0 16 6
A getDataForImport() 0 7 1
A getRowMap() 0 7 1
A makeCurie() 0 11 3
A getDeleteRows() 0 7 1
A getVocabularyStatements() 0 17 1
A getElementSetStatements() 0 23 3
A getUpdateRows() 0 6 1
A logRowError() 0 8 2
A getErrors() 0 3 1
A getStats() 0 12 3

How to fix   Complexity   

Complex Class

Complex classes like DataImporter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DataImporter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/** Created by PhpStorm,  User: jonphipps,  Date: 2017-05-05,  Time: 5:49 PM */
4
5
namespace App\Services\Import;
6
7
use App\Exceptions\DuplicateAttributesException;
8
use App\Exceptions\MissingRequiredAttributeException;
9
use App\Exceptions\UnknownAttributeException;
10
use App\Models\Concept;
11
use App\Models\Element;
12
use App\Models\Export;
13
use Illuminate\Database\Eloquent\Collection as DBCollection;
14
use Illuminate\Support\Collection;
15
use const null;
16
use function collect;
17
18
class DataImporter
19
{
20
    /** @var string $exportName */
21
    private $exportName;
0 ignored issues
show
introduced by
The private property $exportName is not used, and could be removed.
Loading history...
22
    /** @var Collection $data */
23
    private $data;
24
    /** @var Export */
25
    private $export;
26
    /** @var Collection $rowMap */
27
    private $rowMap;
28
    /** @var Collection $rows */
29
    private $rows;
30
    /** @var Collection $addRows */
31
    private $addRows;
32
    /** @var Collection $updateRows */
33
    private $updateRows;
34
    /** @var Collection $updateRows */
35
    private $deleteRows;
36
    /** @var Collection $errors */
37
    private $errors;
38
    /** @var DBCollection $statements */
39
    private $statements;
40
    private $prefixes = [];
41
    private $stats    = [];
42
    /** @var Collection $columnProfileMap */
43
    private $columnProfileMap;
44
    /** @var string */
45
    private $currentColumnName;
46
    /** @var string */
47
    private $currentRowName;
48
49
    public function __construct(Collection $data, Export $export = null)
50
    {
51
        $this->data    = $data;
52
        $columnHeaders = collect($data[0]);
53
        $this->rows    = $this->getDataForImport();
54
55
        $this->export = $export;
56
        if ($export) {
57
            $this->rowMap     = self::getRowMap($export->map);
0 ignored issues
show
Bug introduced by
It seems like $export->map can also be of type null; however, parameter $map of App\Services\Import\DataImporter::getRowMap() does only seem to accept Illuminate\Support\Collection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

57
            $this->rowMap     = self::getRowMap(/** @scrutinizer ignore-type */ $export->map);
Loading history...
58
            try {
59
                $this->columnProfileMap = self::getColumnProfileMap($export, $columnHeaders);
60
            }
61
            //these are all fatal errors
62
            catch (DuplicateAttributesException $e) {
63
                $this->errors = collect(['fatal' => $e->getMessage()]);
64
65
                return;
66
            } catch (MissingRequiredAttributeException $e) {
67
                $this->errors = collect(['fatal' => $e->getMessage()]);
68
69
                return;
70
            } catch (UnknownAttributeException $e) {
71
                $this->errors = collect(['fatal' => $e->getMessage()]);
72
73
                return;
74
            }
75
            $this->errors     = new Collection();
76
            $this->addRows    = $this->getAddRows(); //only gets data rows with no row_id
77
            $this->updateRows = $this->getUpdateRows(); //gets data rows with matching map
78
            $this->deleteRows = $this->getDeleteRows(); //gets map rows with no matching row
79
            if ($export->vocabulary_id) {
80
                $this->prefixes   = $export->vocabulary->prefixes;
81
                $this->statements = $this->getVocabularyStatements();
82
            } else {
83
                $this->prefixes   = $export->elementset->prefixes;
84
                $this->statements = $this->getElementSetStatements();
85
            }
86
        }
87
88
        $this->stats['deleted'] = $this->deleteRows === null ? 0 : $this->deleteRows->count();
89
        $this->stats['updated'] = $this->updateRows === null ? 0 : $this->updateRows->count();
90
        $this->stats['added']   = $this->addRows === null ? 0 : $this->addRows->count();
91
        $this->stats['errors']  = $this->errors === null ? 0 : $this->errors->count();
92
    }
93
94
    /**
95
     * return a collection of rows that have no reg_id.
96
     *
97
     * @return Collection
98
     */
99
    public function getAddRows(): Collection
100
    {
101
        //only keep rows that have no reg_id
102
        return collect($this->rows->filter(function ($row, $key) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

102
        return collect($this->rows->filter(function ($row, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
103
            return empty($row['reg_id']);
104
        }))->values();
105
    }
106
107
    /**
108
     * @return Collection
109
     * @internal param Collection $map
110
     * @internal param array $rowMap
111
     */
112
    public function getChangeset(): Collection
113
    {
114
        $rows       = $this->updateRows;
115
        $rowMap     = $this->rowMap;
116
        $columnMap  = $this->columnProfileMap;
117
        $statements = $this->statements;
118
119
        if ($rows === null) {
120
            return collect([]);
121
        }
122
123
        $changes = $rows->map(function (Collection $row, $key) use ($rowMap, $columnMap, $statements) {
124
            $map          = $rowMap[$key];
125
            $statementRow = $statements[$key];
126
            $this->currentRowName = 'reg_id: ' . $key;
127
128
            return $row->map(function ($value, $column) use ($map, $columnMap, $statementRow) {
129
                // this is to correct for export maps that have '0' for a statement cell reference, but do have data in the statement row
130
                $statementId = ($map->get($column) !== 0) ? $map->get($column) : $column;
131
                $statement   = $statementId ? collect($statementRow->pull($statementId)) : null;
132
                $this->currentColumnName = $column;
133
134
                return [
135
                    'new value'    => $this->validateRequired($value, $column),
136
                    'old value'    => $statement ? $statement->get('old value') : null,
137
                    'statement_id' => $statementId,
138
                    'language'     => $columnMap[$column]['language'],
139
                    'property_id'  => $columnMap[$column]['id'],
140
                    'updated_at'   => $statement ? $statement->get('updated_at') : null,
141
                    'required'     => $column[0] === '*',
142
                ];
143
            })->reject(function ($array) {
144
                return empty($array['new value']) && empty($array['statement_id']); //remove all of the items that have been, and continue to be, empty
145
            })->reject(function ($array, $arrayKey) {
146
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
147
            })->reject(function ($array) {
148
                return $array['new value'] === $array['old value']; //reject every item that has no changes
149
            })->reject(function ($array) {
150
                //reset the URI to be fully qualified
151
                if ($array['statement_id'] === '*uri') {
152
                    $array['new value'] = $this->makeFqn($this->prefixes, $array['new value']);
153
                }
154
155
                return $array['new value'] === $array['old value']; //reject if the URIs match
156
            });
157
        })->reject(function (Collection $items) {
158
            return $items->count() === 0; //reject every row that no longer has items
159
        });
160
161
        $rows      = $this->addRows;
162
        $additions = $rows->map(function (Collection $row, $key) use ($columnMap) {
163
            $this->currentRowName = 'new row: ' . $key;
164
165
            return $row->map(function ($value, $column) use ($columnMap) {
166
                $this->currentColumnName = $column;
167
                //reset the URI to be fully qualified
168
                if ($column === '*uri') {
169
                    $value = $this->makeFqn($this->prefixes, $value);
170
                }
171
172
                return [
173
                   'new value'    => $this->validateRequired($value, $column),
174
                   'old value'    => null,
175
                   'statement_id' => null,
176
                   'language'     => $columnMap[$column]['language'],
177
                   'property_id'  => $columnMap[$column]['id'],
178
                   'updated_at'   => null,
179
                   'required'     => $column[0] === '*',
180
                ];
181
            })->reject(function ($array) {
182
                return empty($array['new value']); //remove all of the items that have been, and continue to be, empty
183
            })->reject(function ($array, $arrayKey) {
184
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
185
            });
186
        })->reject(function (Collection $items) {
187
            return $items->count() === 0; //reject every row that no longer has items
188
        });
189
190
        $changeset['update'] = $changes;
0 ignored issues
show
Comprehensibility Best Practice introduced by
$changeset was never initialized. Although not strictly required by PHP, it is generally a good practice to add $changeset = array(); before regardless.
Loading history...
191
        $changeset['delete'] = $this->deleteRows;
192
        $changeset['add']    = $additions;
193
194
        return collect($changeset);
195
    }
196
197
    /**
198
     * Returns an associative array of data based on the data supplied for import.
199
     *
200
     * @return Collection
201
     */
202
    public function getDataForImport(): Collection
203
    {
204
        $h = $this->data->first();
205
206
        return $this->data->slice(1)->transform(function ($item, $key) use ($h) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

206
        return $this->data->slice(1)->transform(function ($item, /** @scrutinizer ignore-unused */ $key) use ($h) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
207
            return collect($item)->mapWithKeys(function ($item, $key) use ($h) {
208
                return [$h[$key] => $item];
209
            });
210
        });
211
    }
212
213
    /**
214
     * @return Collection
215
     */
216
    public function getDeleteRows(): Collection
217
    {
218
        //only keep rows that are in the rowmap but are missing from the supplied data
219
        $updateRows = $this->updateRows;
220
221
        return collect($this->rowMap->reject(function ($row, $key) use ($updateRows) {
222
            return isset($updateRows[$key]);
223
        }));
224
    }
225
226
    /**
227
     * @param Collection $map
228
     *
229
     * @return Collection
230
     */
231
    public static function getHeaderFromMap(Collection $map): Collection
232
    {
233
        return collect($map->first());
234
    }
235
236
    public function getStats(): Collection
237
    {
238
        $errorCount = 0;
239
        if ($this->errors !== null) {
240
            foreach ($this->errors as $error) {
241
                $errorCount += \count($error);
242
            }
243
        }
244
245
        $this->stats['errors'] = $errorCount;
246
247
        return collect($this->stats);
248
    }
249
250
    /**
251
     * @param Collection $map
252
     *
253
     * @return Collection
254
     */
255
    public static function getRowMap(Collection $map): Collection
256
    {
257
        $p = self::getHeaderFromMap($map);
258
259
        return $map->slice(1)->transform(function ($item, $key) use ($p) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

259
        return $map->slice(1)->transform(function ($item, /** @scrutinizer ignore-unused */ $key) use ($p) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
260
            return collect($item)->mapWithKeys(function ($item, $key) use ($p) {
261
                return [$p[$key]['label'] => $item];
262
            });
263
        });
264
    }
265
266
    /**
267
     * @param Export     $export
268
     * @param Collection $columnHeaders
269
     *
270
     * @return Collection
271
     * @throws DuplicateAttributesException
272
     * @throws MissingRequiredAttributeException
273
     * @throws UnknownAttributeException
274
     */
275
    public static function getColumnProfileMap(Export $export, Collection $columnHeaders): Collection
276
    {
277
        $map        = $export->map;
278
        $profile    = $export->profile;
279
        $mapHeaders = self::getHeaderFromMap($map)->keyBy('label');
0 ignored issues
show
Bug introduced by
It seems like $map can also be of type null; however, parameter $map of App\Services\Import\Data...ter::getHeaderFromMap() does only seem to accept Illuminate\Support\Collection, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

279
        $mapHeaders = self::getHeaderFromMap(/** @scrutinizer ignore-type */ $map)->keyBy('label');
Loading history...
280
        $keys       = $mapHeaders->keys()->toArray();
281
        //get the map for all new columns
282
        $newColumns = $columnHeaders->reject(function ($value, $key) use ($keys) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

282
        $newColumns = $columnHeaders->reject(function ($value, /** @scrutinizer ignore-unused */ $key) use ($keys) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
283
            return in_array($value, $keys, false);
284
        })->map(function ($column) use ($profile) {
285
            return $profile->getColumnMapFromHeader($column);
0 ignored issues
show
Bug introduced by
The method getColumnMapFromHeader() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

285
            return $profile->/** @scrutinizer ignore-call */ getColumnMapFromHeader($column);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
286
        })->keyBy('label');
287
288
        //check for duplicate column headers
289
        $headers = [];
290
        foreach ($columnHeaders as $columnHeader) {
291
            if (isset($headers[$columnHeader])) {
292
                throw new DuplicateAttributesException('"' . $columnHeader . '" is a duplicate attribute column. Columns cannot be duplicated');
293
            } else {
294
                $headers[$columnHeader] = $columnHeader;
295
            }
296
        }
297
298
        //check for unknown columns
299
        if (count($newColumns)) {
300
            if (count($newColumns) > 1) {
301
                $unknown = 'columns: ';
302
                foreach ($newColumns as $item) {
303
                    $unknown .= '"' . $item['label'] . '", ';
304
                }
305
                $unknown = rtrim($unknown, ', ') . ' ...are';
306
            } else {
307
                $unknown = 'column: ';
308
                foreach ($newColumns as $item) {
309
                    $unknown .= '"' . $item['label'] . '" ...is';
310
                }
311
            }
312
            throw new UnknownAttributeException('The ' . $unknown . ' unknown and need to be registered with the Profile');
313
        }
314
315
        //check for missing required columns
316
        $missingRequired = collect($keys)->diff($columnHeaders)->filter(function ($value, $key) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

316
        $missingRequired = collect($keys)->diff($columnHeaders)->filter(function ($value, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
317
            return $value !== ltrim($value, '*');
318
        });
319
        if (count($missingRequired)) {
320
            if (count($missingRequired) > 1) {
321
                $missing = 'columns: ';
322
                foreach ($missingRequired as $item) {
323
                    $missing .= '"' . $item . '", ';
324
                }
325
                $missing = rtrim($missing, ', ') . ' ...are';
326
            } else {
327
                $missing = 'column: ';
328
                foreach ($missingRequired as $item) {
329
                    $missing .= '"' . $item . '" ...is';
330
                }
331
            }
332
            throw new MissingRequiredAttributeException('The required attribute ' . $missing . ' missing');
333
        }
334
335
        return $mapHeaders->merge($newColumns);
336
    }
337
338
    /**
339
     * @return Collection
340
     */
341
    public function getUpdateRows(): Collection
342
    {
343
        //only keep rows that have a non-empty reg_id
344
        return $this->rows->reject(function ($row) {
345
            return empty($row['reg_id']);
346
        })->keyBy('reg_id');
347
    }
348
349
    /**
350
     * @return Collection
351
     */
352
    public function getErrors(): Collection
353
    {
354
        return $this->errors;
355
    }
356
357
    /**
358
     * @return Collection
359
     */
360
    public function getVocabularyStatements(): Collection
361
    {
362
        return Concept::whereVocabularyId($this->export->vocabulary_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($concept, $key) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

362
        return Concept::whereVocabularyId($this->export->vocabulary_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($concept, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
363
            return $concept->statements->keyBy('id')->map(function ($property) {
364
                return [
365
                    'old value'  => $property->object,
366
                    'updated_at' => $property->updated_at,
367
                ];
368
            })->prepend([
369
                'old value'  => $concept->uri,
370
                'updated_at' => $concept->updated_at,
371
            ],
372
                '*uri')->prepend([
373
                    'old value'  => $concept->status->display_name,
374
                    'updated_at' => $concept->updated_at,
375
                ],
376
                    '*status');
377
        });
378
    }
379
380
    /**
381
     * @return Collection
382
     */
383
    public function getElementSetStatements(): Collection
384
    {
385
        return Element::whereSchemaId($this->export->schema_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($element, $key) {
1 ignored issue
show
Unused Code introduced by
The parameter $key is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

385
        return Element::whereSchemaId($this->export->schema_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($element, /** @scrutinizer ignore-unused */ $key) {

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
386
            $status = $element->status->display_name;
387
            $thingy = $element->statements->keyBy('id')->map(function ($property) {
388
                return [
389
                    'old value'   => $property->object,
390
                    'updated_at'  => $property->updated_at,
391
                    'profile_uri' => $property->profile_property->uri,
392
                    'is_resource' => (bool) $property->profile_property->is_object_prop,
393
                ];
394
            })->map(function ($item) use ($status) {
395
                if ($item['profile_uri'] === 'reg:status') {
396
                    $item['old value'] = $status;
397
                }
398
                if ($item['is_resource']) {
399
                    $item['old value'] = self::makeCurie($this->prefixes, $item['old value']);
400
                }
401
402
                return $item;
403
            });
404
405
            return $thingy;
406
        });
407
    }
408
409
    /**
410
     * @param $message
411
     *
412
     * @return string
413
     */
414
    protected static function makeErrorMessage($message): string
415
    {
416
        return "[ERROR: $message]";
417
    }
418
419
    /**
420
     * @param $value
421
     * @param $column
422
     * @param $row
423
     * @param $level
424
     */
425
    private function logRowError($value, $column, $row, $level): void
426
    {
427
        //if this is the first error, initialize the row errors
428
        if (! $this->errors->get('row')) {
429
            $this->errors->put('row', collect());
430
        }
431
432
        $this->errors->get('row')->push(collect([$row, $column, $value, $level]));
433
    }
434
435
    /**
436
     * @param array  $prefixes
437
     * @param string $uri
438
     *
439
     * @return string
440
     */
441
    private function makeFqn($prefixes, $uri): string
442
    {
443
        $result = $uri;
444
        foreach ($prefixes as $prefix => $fullUri) {
445
            $result = preg_replace('#' . $prefix . ':#uis', $fullUri, $uri);
446
            if ($result !== $uri) {
447
                break;
448
            }
449
        }
450
        if ($uri === $result && strpos($uri, ':') && ! strpos($uri, '://')) {
451
            //we have an unregistered prefix
452
            $prefix = str_before($uri, ':');
453
            $this->logRowError(self::makeErrorMessage("'$prefix' is an unregistered prefix and cannot be expanded to form a full URI"), $this->currentColumnName, $this->currentRowName, 'warning');
454
        }
455
456
        return $result;
457
    }
458
459
    /**
460
     * @param array  $prefixes
461
     * @param string $uri
462
     *
463
     * @return string
464
     */
465
    private static function makeCurie($prefixes, $uri): string
466
    {
467
        $result = $uri;
468
        foreach ($prefixes as $prefix => $fullUri) {
469
            $result = preg_replace('!' . $fullUri . '!uis', $prefix . ':', $uri);
470
            if ($result !== $uri) {
471
                break;
472
            }
473
        }
474
475
        return $result;
476
    }
477
478
    /**
479
     * @param          $value
480
     * @param          $column
481
     *
482
     * @return string
483
     */
484
    private function validateRequired($value, $column): string
485
    {
486
        if (empty($value) && $column[0] === '*') {
487
            $value = self::makeErrorMessage('Empty required attribute');
488
            $this->logRowError($value, $column, $this->currentRowName, 'fatal');
489
        }
490
491
        return $value;
492
    }
493
}
494