Passed
Push — stage ( 1ed9d9...f7313c )
by Jon
07:42
created

DataImporter::setStats()   B

Complexity

Conditions 5
Paths 16

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 6
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 4
nc 16
nop 0
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
                $this->setStats();
65
66
                return;
67
            } catch (MissingRequiredAttributeException $e) {
68
                $this->errors = collect(['fatal' => $e->getMessage()]);
69
                $this->setStats();
70
71
                return;
72
            } catch (UnknownAttributeException $e) {
73
                $this->errors = collect(['fatal' => $e->getMessage()]);
74
                $this->setStats();
75
76
                return;
77
            }
78
            $this->errors     = new Collection();
79
            $this->addRows    = $this->getAddRows(); //only gets data rows with no row_id
80
            $this->updateRows = $this->getUpdateRows(); //gets data rows with matching map
81
            $this->deleteRows = $this->getDeleteRows(); //gets map rows with no matching row
82
            if ($export->vocabulary_id) {
83
                $this->prefixes   = $export->vocabulary->prefixes;
84
                $this->statements = $this->getVocabularyStatements();
85
            } else {
86
                $this->prefixes   = $export->elementset->prefixes;
87
                $this->statements = $this->getElementSetStatements();
88
            }
89
        }
90
91
        $this->setStats();
92
    }
93
94
    public function setStats()
95
    {
96
        $this->stats['deleted'] = $this->deleteRows === null ? 0 : $this->deleteRows->count();
97
        $this->stats['updated'] = $this->updateRows === null ? 0 : $this->updateRows->count();
98
        $this->stats['added']   = $this->addRows === null ? 0 : $this->addRows->count();
99
        $this->stats['errors']  = $this->errors === null ? 0 : $this->errors->count();
100
    }
101
    /**
102
     * return a collection of rows that have no reg_id.
103
     *
104
     * @return Collection
105
     */
106
    public function getAddRows(): Collection
107
    {
108
        //only keep rows that have no reg_id
109
        return collect($this->rows->filter(function ($row, $key) {
110
            return empty($row['reg_id']);
111
        }))->values();
112
    }
113
114
    /**
115
     * @return Collection
116
     * @internal param Collection $map
117
     * @internal param array $rowMap
118
     */
119
    public function getChangeset(): Collection
120
    {
121
        $rows       = $this->updateRows;
122
        $rowMap     = $this->rowMap;
123
        $columnMap  = $this->columnProfileMap;
124
        $statements = $this->statements;
125
126
        if ($rows === null) {
127
            return collect([]);
128
        }
129
130
        $changes = $rows->map(function (Collection $row, $key) use ($rowMap, $columnMap, $statements) {
131
            $map          = $rowMap[$key];
132
            $statementRow = $statements[$key];
133
            $this->currentRowName = 'reg_id: ' . $key;
134
135
            return $row->map(function ($value, $column) use ($map, $columnMap, $statementRow) {
136
                // this is to correct for export maps that have '0' for a statement cell reference, but do have data in the statement row
137
                $statementId = ($map->get($column) !== 0) ? $map->get($column) : $column;
138
                $statement   = $statementId ? collect($statementRow->pull($statementId)) : null;
139
                $this->currentColumnName = $column;
140
141
                return [
142
                    'new value'    => $this->validateRequired($value, $column),
143
                    'old value'    => $statement ? $statement->get('old value') : null,
144
                    'statement_id' => $statementId,
145
                    'language'     => $columnMap[$column]['language'],
146
                    'property_id'  => $columnMap[$column]['id'],
147
                    'updated_at'   => $statement ? $statement->get('updated_at') : null,
148
                    'required'     => $column[0] === '*',
149
                ];
150
            })->reject(function ($array) {
151
                return empty($array['new value']) && empty($array['statement_id']); //remove all of the items that have been, and continue to be, empty
152
            })->reject(function ($array, $arrayKey) {
153
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
154
            })->reject(function ($array) {
155
                return $array['new value'] === $array['old value']; //reject every item that has no changes
156
            })->reject(function ($array) {
157
                //reset the URI to be fully qualified
158
                if ($array['statement_id'] === '*uri') {
159
                    $array['new value'] = $this->makeFqn($this->prefixes, $array['new value']);
160
                }
161
162
                return $array['new value'] === $array['old value']; //reject if the URIs match
163
            });
164
        })->reject(function (Collection $items) {
165
            return $items->count() === 0; //reject every row that no longer has items
166
        });
167
168
        $rows      = $this->addRows;
169
        $additions = $rows->map(function (Collection $row, $key) use ($columnMap) {
170
            $this->currentRowName = 'new row: ' . $key;
171
172
            return $row->map(function ($value, $column) use ($columnMap) {
173
                $this->currentColumnName = $column;
174
                //reset the URI to be fully qualified
175
                if ($column === '*uri') {
176
                    $value = $this->makeFqn($this->prefixes, $value);
177
                }
178
179
                return [
180
                   'new value'    => $this->validateRequired($value, $column),
181
                   'old value'    => null,
182
                   'statement_id' => null,
183
                   'language'     => $columnMap[$column]['language'],
184
                   'property_id'  => $columnMap[$column]['id'],
185
                   'updated_at'   => null,
186
                   'required'     => $column[0] === '*',
187
                ];
188
            })->reject(function ($array) {
189
                return empty($array['new value']); //remove all of the items that have been, and continue to be, empty
190
            })->reject(function ($array, $arrayKey) {
191
                return $arrayKey === 'reg_id'; //remove all of the reg_id items
192
            });
193
        })->reject(function (Collection $items) {
194
            return $items->count() === 0; //reject every row that no longer has items
195
        });
196
197
        $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...
198
        $changeset['delete'] = $this->deleteRows;
199
        $changeset['add']    = $additions;
200
201
        return collect($changeset);
202
    }
203
204
    /**
205
     * Returns an associative array of data based on the data supplied for import.
206
     *
207
     * @return Collection
208
     */
209
    public function getDataForImport(): Collection
210
    {
211
        $h = $this->data->first();
212
213
        return $this->data->slice(1)->transform(function ($item, $key) use ($h) {
214
            return collect($item)->mapWithKeys(function ($item, $key) use ($h) {
215
                return [$h[$key] => $item];
216
            });
217
        });
218
    }
219
220
    /**
221
     * @return Collection
222
     */
223
    public function getDeleteRows(): Collection
224
    {
225
        //only keep rows that are in the rowmap but are missing from the supplied data
226
        $updateRows = $this->updateRows;
227
228
        return collect($this->rowMap->reject(function ($row, $key) use ($updateRows) {
229
            return isset($updateRows[$key]);
230
        }));
231
    }
232
233
    /**
234
     * @param Collection $map
235
     *
236
     * @return Collection
237
     */
238
    public static function getHeaderFromMap(Collection $map): Collection
239
    {
240
        return collect($map->first());
241
    }
242
243
    public function getStats(): Collection
244
    {
245
        $errorCount = 0;
246
        if ($this->errors !== null) {
247
            foreach ($this->errors as $error) {
248
                $errorCount += \count($error);
249
            }
250
        }
251
252
        $this->stats['errors'] = $errorCount;
253
254
        return collect($this->stats);
255
    }
256
257
    /**
258
     * @param Collection $map
259
     *
260
     * @return Collection
261
     */
262
    public static function getRowMap(Collection $map): Collection
263
    {
264
        $p = self::getHeaderFromMap($map);
265
266
        return $map->slice(1)->transform(function ($item, $key) use ($p) {
267
            return collect($item)->mapWithKeys(function ($item, $key) use ($p) {
268
                return [$p[$key]['label'] => $item];
269
            });
270
        });
271
    }
272
273
    /**
274
     * @param Export     $export
275
     * @param Collection $columnHeaders
276
     *
277
     * @return Collection
278
     * @throws DuplicateAttributesException
279
     * @throws MissingRequiredAttributeException
280
     * @throws UnknownAttributeException
281
     */
282
    public static function getColumnProfileMap(Export $export, Collection $columnHeaders): Collection
283
    {
284
        $map        = $export->map;
285
        $profile    = $export->profile;
286
        $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

286
        $mapHeaders = self::getHeaderFromMap(/** @scrutinizer ignore-type */ $map)->keyBy('label');
Loading history...
287
        $keys       = $mapHeaders->keys()->toArray();
288
        //get the map for all new columns
289
        $newColumns = $columnHeaders->reject(function ($value, $key) use ($keys) {
290
            return in_array($value, $keys, false);
291
        })->map(function ($column) use ($profile) {
292
            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

292
            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...
293
        })->keyBy('label');
294
295
        //check for duplicate column headers
296
        $headers = [];
297
        foreach ($columnHeaders as $columnHeader) {
298
            if (isset($headers[$columnHeader])) {
299
                throw new DuplicateAttributesException('"' . $columnHeader . '" is a duplicate attribute column. Columns cannot be duplicated');
300
            } else {
301
                $headers[$columnHeader] = $columnHeader;
302
            }
303
        }
304
305
        //check for unknown columns
306
        if (count($newColumns)) {
307
            if (count($newColumns) > 1) {
308
                $unknown = 'columns: ';
309
                foreach ($newColumns as $item) {
310
                    $unknown .= '"' . $item['label'] . '", ';
311
                }
312
                $unknown = rtrim($unknown, ', ') . ' ...are';
313
            } else {
314
                $unknown = 'column: ';
315
                foreach ($newColumns as $item) {
316
                    $unknown .= '"' . $item['label'] . '" ...is';
317
                }
318
            }
319
            throw new UnknownAttributeException('The ' . $unknown . ' unknown and need to be registered with the Profile');
320
        }
321
322
        //check for missing required columns
323
        $missingRequired = collect($keys)->diff($columnHeaders)->filter(function ($value, $key) {
324
            return $value !== ltrim($value, '*');
325
        });
326
        if (count($missingRequired)) {
327
            if (count($missingRequired) > 1) {
328
                $missing = 'columns: ';
329
                foreach ($missingRequired as $item) {
330
                    $missing .= '"' . $item . '", ';
331
                }
332
                $missing = rtrim($missing, ', ') . ' ...are';
333
            } else {
334
                $missing = 'column: ';
335
                foreach ($missingRequired as $item) {
336
                    $missing .= '"' . $item . '" ...is';
337
                }
338
            }
339
            throw new MissingRequiredAttributeException('The required attribute ' . $missing . ' missing');
340
        }
341
342
        return $mapHeaders->merge($newColumns);
343
    }
344
345
    /**
346
     * @return Collection
347
     */
348
    public function getUpdateRows(): Collection
349
    {
350
        //only keep rows that have a non-empty reg_id
351
        return $this->rows->reject(function ($row) {
352
            return empty($row['reg_id']);
353
        })->keyBy('reg_id');
354
    }
355
356
    /**
357
     * @return Collection
358
     */
359
    public function getErrors(): Collection
360
    {
361
        return $this->errors;
362
    }
363
364
    /**
365
     * @return Collection
366
     */
367
    public function getVocabularyStatements(): Collection
368
    {
369
        return Concept::whereVocabularyId($this->export->vocabulary_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($concept, $key) {
370
            return $concept->statements->keyBy('id')->map(function ($property) {
371
                return [
372
                    'old value'  => $property->object,
373
                    'updated_at' => $property->updated_at,
374
                ];
375
            })->prepend([
376
                'old value'  => $concept->uri,
377
                'updated_at' => $concept->updated_at,
378
            ],
379
                '*uri')->prepend([
380
                    'old value'  => $concept->status->display_name,
381
                    'updated_at' => $concept->updated_at,
382
                ],
383
                    '*status');
384
        });
385
    }
386
387
    /**
388
     * @return Collection
389
     */
390
    public function getElementSetStatements(): Collection
391
    {
392
        return Element::whereSchemaId($this->export->schema_id)->with('statements.profile_property', 'status')->get()->keyBy('id')->map(function ($element, $key) {
393
            $status = $element->status->display_name;
394
            $thingy = $element->statements->keyBy('id')->map(function ($property) {
395
                return [
396
                    'old value'   => $property->object,
397
                    'updated_at'  => $property->updated_at,
398
                    'profile_uri' => $property->profile_property->uri,
399
                    'is_resource' => (bool) $property->profile_property->is_object_prop,
400
                ];
401
            })->map(function ($item) use ($status) {
402
                if ($item['profile_uri'] === 'reg:status') {
403
                    $item['old value'] = $status;
404
                }
405
                if ($item['is_resource']) {
406
                    $item['old value'] = self::makeCurie($this->prefixes, $item['old value']);
407
                }
408
409
                return $item;
410
            });
411
412
            return $thingy;
413
        });
414
    }
415
416
    /**
417
     * @param $message
418
     *
419
     * @return string
420
     */
421
    protected static function makeErrorMessage($message): string
422
    {
423
        return "[ERROR: $message]";
424
    }
425
426
    /**
427
     * @param $value
428
     * @param $column
429
     * @param $row
430
     * @param $level
431
     */
432
    private function logRowError($value, $column, $row, $level): void
433
    {
434
        //if this is the first error, initialize the row errors
435
        if (! $this->errors->get('row')) {
436
            $this->errors->put('row', collect());
437
        }
438
439
        $this->errors->get('row')->push(collect([$row, $column, $value, $level]));
440
    }
441
442
    /**
443
     * @param array  $prefixes
444
     * @param string $uri
445
     *
446
     * @return string
447
     */
448
    private function makeFqn($prefixes, $uri): string
449
    {
450
        $result = $uri;
451
        foreach ($prefixes as $prefix => $fullUri) {
452
            $result = preg_replace('#' . $prefix . ':#uis', $fullUri, $uri);
453
            if ($result !== $uri) {
454
                break;
455
            }
456
        }
457
        if ($uri === $result && strpos($uri, ':') && ! strpos($uri, '://')) {
458
            //we have an unregistered prefix
459
            $prefix = str_before($uri, ':');
460
            $this->logRowError(self::makeErrorMessage("'$prefix' is an unregistered prefix and cannot be expanded to form a full URI"), $this->currentColumnName, $this->currentRowName, 'warning');
461
        }
462
463
        return $result;
464
    }
465
466
    /**
467
     * @param array  $prefixes
468
     * @param string $uri
469
     *
470
     * @return string
471
     */
472
    private static function makeCurie($prefixes, $uri): string
473
    {
474
        $result = $uri;
475
        foreach ($prefixes as $prefix => $fullUri) {
476
            $result = preg_replace('!' . $fullUri . '!uis', $prefix . ':', $uri);
477
            if ($result !== $uri) {
478
                break;
479
            }
480
        }
481
482
        return $result;
483
    }
484
485
    /**
486
     * @param          $value
487
     * @param          $column
488
     *
489
     * @return string
490
     */
491
    private function validateRequired($value, $column): string
492
    {
493
        if (empty($value) && $column[0] === '*') {
494
            $value = self::makeErrorMessage('Empty required attribute');
495
            $this->logRowError($value, $column, $this->currentRowName, 'fatal');
496
        }
497
498
        return $value;
499
    }
500
}
501