GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Completed
Pull Request — development (#846)
by
unknown
08:14
created

Elements::createGroup()   B

Complexity

Conditions 6
Paths 8

Size

Total Lines 30
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 13
nc 8
nop 3
dl 0
loc 30
rs 8.439
c 0
b 0
f 0
1
<?php
2
3
namespace SleepingOwl\Admin\Form\Related;
4
5
use DB;
6
use Validator;
7
use Illuminate\Http\Request;
8
use Admin\Contracts\HasFakeModel;
9
use Illuminate\Support\Collection;
10
use Illuminate\Database\Eloquent\Model;
11
use SleepingOwl\Admin\Form\FormElements;
12
use KodiComponents\Support\HtmlAttributes;
13
use SleepingOwl\Admin\Contracts\Initializable;
14
use SleepingOwl\Admin\Form\Element\NamedFormElement;
15
use Illuminate\Database\Eloquent\ModelNotFoundException;
16
use Illuminate\Database\Eloquent\Relations\HasOneOrMany;
17
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
18
use Illuminate\Database\Eloquent\RelationNotFoundException;
19
20
abstract class Elements extends FormElements
21
{
22
    use HtmlAttributes, HasUniqueValidation;
23
24
    protected $view = 'form.element.related.elements';
25
26
    const NEW_ITEM = 'new';
27
28
    const REMOVE = 'remove';
29
30
    /**
31
     * How many items can be created.
32
     *
33
     * @var int
34
     */
35
    protected $limit;
36
37
    /**
38
     * Relation name of the model.
39
     *
40
     * @var string
41
     */
42
    protected $relationName;
43
44
    protected $new = 0;
45
46
    /**
47
     * @var string
48
     */
49
    protected $groupLabel;
50
51
    /**
52
     * Main label of dynamic form.
53
     *
54
     * @var string
55
     */
56
    protected $label;
57
58
    /**
59
     * Loaded related values.
60
     *
61
     * @var \Illuminate\Support\Collection
62
     */
63
    protected $relatedValues;
64
65
    /**
66
     * @var \Illuminate\Database\Eloquent\Model
67
     */
68
    protected $instance;
69
70
    protected $stubElements;
71
72
    protected $toRemove;
73
74
    protected $emptyRelation;
75
76
    protected $unique;
77
78
    /**
79
     * @var
80
     */
81
    protected $groups;
82
83
    protected $queryCallbacks = [];
84
85
    public function __construct($relationName, array $elements = [])
86
    {
87
        $this->toRemove = collect();
88
        $this->groups = collect();
89
        $this->relatedValues = collect();
90
        parent::__construct($elements);
91
92
        $this->setRelationName($relationName);
93
    }
94
95
    /**
96
     * @param int $limit
97
     *
98
     * @return $this
99
     */
100
    public function setLimit($limit)
101
    {
102
        $this->limit = $limit;
103
104
        return $this;
105
    }
106
107
    public function modifyQuery(callable $callback)
108
    {
109
        $this->queryCallbacks[] = $callback;
110
111
        return $this;
112
    }
113
114
    public function setLabel($label)
115
    {
116
        $this->label = $label;
117
118
        return $this;
119
    }
120
121
    public function initialize()
122
    {
123
        parent::initialize();
124
        $this->checkRelationOfModel();
125
126
        $this->stubElements = $this->getNewElements();
127
        $this->forEachElement($this->stubElements, function ($element) {
128
            $element->setDefaultValue(null);
129
            if (! $element instanceof HasFakeModel) {
130
                $element->setPath('');
131
            }
132
        });
133
134
        $this->initializeRemoveEntities();
135
    }
136
137
    protected function initializeRemoveEntities()
138
    {
139
        $key = $this->relationName.'.'.static::REMOVE;
140
141
        $this->toRemove = collect(request()->input($key, []));
142
143
        request()->replace(array_except(request()->all(), $this->relationName.'.'.static::REMOVE));
144
    }
145
146
    /**
147
     * @return void
148
     */
149
    public function initializeElements()
150
    {
151
        $this->getElements()->each(function ($element) {
152
            if ($element instanceof Initializable) {
153
                $element->initialize();
154
            }
155
156
            if ($element instanceof HasFakeModel) {
157
                $element->setFakeModel($this->getModel());
158
            }
159
        });
160
    }
161
162
    /**
163
     * @param $item
164
     *
165
     * @param array $columns
166
     *
167
     * @return string
168
     */
169
    protected function getCompositeKey($item, array $columns)
170
    {
171
        $primaries = [];
172
        foreach ($columns as $name) {
173
            $primaries[] = $item[$name];
174
        }
175
176
        return implode('_', $primaries);
177
    }
178
179
    protected function makeValidationAttribute($name)
180
    {
181
        return $this->relationName.'.*.'.$name;
182
    }
183
184
    protected function getNewElements()
185
    {
186
        return $this->cloneElements($this);
187
    }
188
189
    protected function cloneElements(FormElements $element)
190
    {
191
        $elements = clone $element->getElements()->map(function ($element) {
192
            return clone $element;
193
        });
194
195
        return $elements->map(function ($element) {
196
            return $this->emptyElement($element);
197
        });
198
    }
199
200
    protected function emptyElement($element)
201
    {
202
        $el = clone $element;
203
        if ($el instanceof \SleepingOwl\Admin\Form\Columns\Columns) {
204
            $col = new Columns();
205
            $columns = $el->getElements();
206
            $col->setElements((clone $columns)->map(function ($column) {
207
                return $this->emptyElement($column);
208
            })->all());
209
210
            return $col;
211
        }
212
213
        if ($el instanceof FormElements) {
214
            $el->setElements($this->cloneElements($el)->all());
215
        } else {
216
            $el->setDefaultValue(null);
217
            $el->setValueSkipped(true);
218
        }
219
220
        return $el;
221
    }
222
223
    public function setModel(Model $model)
224
    {
225
        parent::setModel($model);
226
227
        if ($model->exists) {
228
            $this->setInstance($model);
229
            $this->loadRelationValues();
230
        }
231
    }
232
233
    public function setInstance($instance)
234
    {
235
        $this->instance = $instance;
236
    }
237
238
    protected function checkRelationOfModel()
239
    {
240
        $model = $this->getModel();
241
        $class = get_class($model);
242
        if (! method_exists($model, $this->relationName)) {
243
            throw new RelationNotFoundException("Relation {$this->relationName} doesn't exist on {$class}");
244
        }
245
246
        $relation = $model->{$this->relationName}();
247
        if (! ($relation instanceof BelongsToMany) && ! ($relation instanceof HasOneOrMany)) {
248
            throw new \InvalidArgumentException("Relation {$this->relationName} of model {$class} must be instance of HasMany or BelongsToMany");
249
        }
250
    }
251
252
    /**
253
     * Sets relation name property.
254
     *
255
     * @param string
256
     *
257
     * @return $this
258
     */
259
    public function setRelationName($name)
260
    {
261
        $this->relationName = $name;
262
263
        return $this;
264
    }
265
266
    protected function loadRelationValues()
267
    {
268
        if (! $this->instance) {
269
            throw new ModelNotFoundException("Model {$this->getModel()} wasn't found for loading relation");
270
        }
271
272
        $query = $this->getRelation();
273
        if (count($this->queryCallbacks) > 0) {
274
            foreach ($this->queryCallbacks as $callback) {
275
                $callback($query);
276
            }
277
        }
278
279
        $this->relatedValues = $this->retrieveRelationValuesFromQuery($query);
280
    }
281
282
    /**
283
     * @param \Illuminate\Http\Request|null $request
284
     *
285
     * @return array
286
     */
287
    protected function getRequestData(Request $request = null)
288
    {
289
        return $request ? $request->get($this->relationName, []) : old($this->relationName, []);
290
    }
291
292
    protected function buildGroupsCollection()
293
    {
294
        $relatedValues = $this->relatedValues;
295
296
        if (count($old = $this->getRequestData()) !== 0) {
297
            $relatedValues = $this->getRelatedValuesFromRequestData($old);
298
            $old = true;
299
        }
300
301
        foreach ($relatedValues as $key => $item) {
302
            $this->groups->push($this->createGroup($item, $old, $key));
303
        }
304
    }
305
306
    protected function getRelatedValuesFromRequestData(array $values)
307
    {
308
        $collection = collect();
309
        foreach ($values as $key => $attributes) {
310
            if (strpos($key, static::NEW_ITEM) !== false) {
311
                $this->new++;
312
            }
313
314
            if ($this->relatedValues->has($key)) {
315
                $attributes = $this->safeFillModel($this->relatedValues->get($key), $attributes);
316
            }
317
318
            $collection->put($key, $attributes);
319
        }
320
321
        return $collection;
322
    }
323
324
    protected function createGroup($attributes, $old = false, $key = null)
325
    {
326
        $model = $attributes instanceof Model ? $attributes : $this->safeCreateModel($this->getModelClassForElements(), $attributes);
327
        $group = new Group($model);
328
329
        if ($this->groupLabel) {
330
            $group->setLabel($this->groupLabel);
331
        }
332
333
        $this->forEachElement($elements = $this->getNewElements(), function (NamedFormElement $el) use ($model, $key, $old) {
334
            // Setting default value, name and model for element with name attribute
335
            $el->setDefaultValue($el->prepareValue($model->getAttribute($el->getModelAttributeKey())));
336
            $el->setName(sprintf('%s[%s][%s]', $this->relationName, $key ?: $model->getKey(), $this->formatElementName($el->getName())));
337
            $el->setModel($model);
338
339
            if ($old) {
340
                // If there were old values (validation fail, etc.) each element must have different path to get the old
341
                // value. If we don't do it, there will be collision if two elements with same name present in main form
342
                // and related form. For example: we have "Company" and "Shop" models with field "name" and include HasMany
343
                // form with company's shops inside "Companies" section. There will be collisions of "name" if validation
344
                // fails, and each "shop"-form will have "company->name" value inside "name" field.
345
                $el->setPath($el->getName());
346
            }
347
        });
348
349
        foreach ($elements as $el) {
350
            $group->push($el);
351
        }
352
353
        return $group;
354
    }
355
356
    protected function formatElementName($name)
357
    {
358
        return preg_replace("/{$this->relationName}\[[\w]+\]\[(.+?)\]/", '$1', $name);
359
    }
360
361
    protected function forEachElement(Collection $elements, $callback)
362
    {
363
        foreach ($this->flatNamedElements($elements) as $element) {
364
            $callback($element);
365
        }
366
    }
367
368
    protected function flatNamedElements(Collection $elements)
369
    {
370
        return $elements->reduce(function (Collection $initial, $element) {
371
            if ($element instanceof NamedFormElement) {
372
                $initial->push($element);
373
            } elseif ($element instanceof FormElements) {
374
                return $initial->merge($this->flatNamedElements($element->getElements()));
375
            }
376
377
            return $initial;
378
        }, collect());
379
    }
380
381
    protected function safeCreateModel($modelClass, array $attributes = [])
382
    {
383
        return $this->safeFillModel(new $modelClass, $attributes);
384
    }
385
386
    protected function safeFillModel(Model $model, array $attributes = [])
387
    {
388
        foreach ($attributes as $attribute => $value) {
389
            $model->setAttribute($attribute, $value);
390
        }
391
392
        return $model;
393
    }
394
395
    /**
396
     * @return \Illuminate\Database\Eloquent\Relations\Relation
397
     */
398
    protected function getEmptyRelation()
399
    {
400
        return $this->emptyRelation ?: $this->emptyRelation = $this->getModel()
401
            ->{$this->relationName}();
402
    }
403
404
    protected function getRelation()
405
    {
406
        return $this->instance->{$this->relationName}();
407
    }
408
409
    public function save(Request $request)
410
    {
411
        $this->prepareRelatedValues($this->getRequestData($request));
412
413
        $this->transactionLevel = DB::transactionLevel();
0 ignored issues
show
Bug Best Practice introduced by
The property transactionLevel does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
414
        DB::beginTransaction();
415
        // Nothing to do here...
416
    }
417
418
    public function getValidationRulesFromElements(array $rules = [])
419
    {
420
        $this->flatNamedElements($this->getElements())->each(function ($element) use (&$rules) {
421
            $rules += $this->modifyValidationParameters($element->getValidationRules());
422
        });
423
424
        return $rules;
425
    }
426
427
    public function getValidationMessagesForElements(array $messages = [])
428
    {
429
        $this->flatNamedElements($this->getElements())->each(function ($element) use (&$messages) {
430
            $messages += $this->modifyValidationParameters($element->getValidationMessages());
431
        });
432
433
        return $messages;
434
    }
435
436
    public function afterSave(Request $request)
437
    {
438
        try {
439
            // By this time getModel method will always return existed model object, not empty
440
            // so wee need to fresh it, because if it's new model creating relation will throw
441
            // exception 'call relation method on null'
442
            $this->setInstance($this->getModel());
443
            $this->proceedSave($request);
444
            DB::commit();
445
        } catch (\Throwable $exception) {
446
            \Session::flash('success_message', 'Произошла ошибка сохранения');
447
            DB::rollBack($this->transactionLevel);
0 ignored issues
show
Unused Code introduced by
The call to Illuminate\Support\Facades\DB::rollBack() has too many arguments starting with $this->transactionLevel. ( Ignorable by Annotation )

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

447
            DB::/** @scrutinizer ignore-call */ 
448
                rollBack($this->transactionLevel);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
448
449
            throw $exception;
450
        }
451
    }
452
453
    /**
454
     * Returns model class for each element in form.
455
     *
456
     * @return string
457
     */
458
    protected function getModelClassForElements()
459
    {
460
        return get_class($this->getModelForElements());
461
    }
462
463
    protected function modifyValidationParameters(array $parameters)
464
    {
465
        $result = [];
466
        foreach ($parameters as $name => $parameter) {
467
            $result["{$this->relationName}.*.{$name}"] = $parameter;
468
        }
469
470
        return $result;
471
    }
472
473
    /**
474
     * Get the instance as an array.
475
     *
476
     * @return array
477
     */
478
    public function toArray()
479
    {
480
        $this->buildGroupsCollection();
481
482
        return parent::toArray() + [
483
                'stub'             => $this->stubElements,
484
                'name'             => $this->relationName,
485
                'label'            => $this->label,
486
                'groups'           => $this->groups,
487
                'remove'           => $this->toRemove,
488
                'newEntitiesCount' => $this->new,
489
                'limit'            => $this->limit,
490
            ];
491
    }
492
493
    /**
494
     * @param string $groupLabel
495
     *
496
     * @return $this
497
     */
498
    public function setGroupLabel($groupLabel)
499
    {
500
        $this->groupLabel = $groupLabel;
501
502
        return $this;
503
    }
504
505
    /**
506
     * Checks if count of
507
     *
508
     * @return int
509
     */
510
    public function exceedsLimit()
511
    {
512
        if ($this->limit === null) {
513
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The expression return false returns the type false which is incompatible with the documented return type integer.
Loading history...
514
        }
515
516
        return $this->relatedValues->count() >= $this->limit;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->relatedVal...count() >= $this->limit returns the type boolean which is incompatible with the documented return type integer.
Loading history...
517
    }
518
519
    /**
520
     * Appends fresh related model if total count is not exceeding limit.
521
     *
522
     * @param $key
523
     *
524
     * @return Model
525
     */
526
    protected function addOrGetRelated($key)
527
    {
528
        $related = $this->relatedValues->get($key) ?: $this->getFreshModelForElements();
529
530
        if (! $related->exists && ! $this->exceedsLimit()) {
531
            $this->relatedValues->put($key, $related);
532
        }
533
534
        return $related;
535
    }
536
537
    /**
538
     * @return $this
539
     */
540
    public function disableCreation()
541
    {
542
        $this->setLimit(0);
543
544
        return $this;
545
    }
546
547
    abstract protected function retrieveRelationValuesFromQuery($query);
548
549
    /**
550
     * Returns model for each element in form.
551
     *
552
     * @return \Illuminate\Database\Eloquent\Model
553
     */
554
    abstract protected function getModelForElements();
555
556
    /**
557
     * Returns fresh instance of model for each element in form.
558
     *
559
     * @return \Illuminate\Database\Eloquent\Model
560
     */
561
    abstract protected function getFreshModelForElements();
562
563
    abstract protected function proceedSave(Request $request);
564
565
    abstract protected function prepareRelatedValues(array $data);
566
}
567