Test Failed
Push — master ( fae1a9...cc5ef8 )
by Terzi
04:02
created

Saver::connection()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Terranet\Administrator\Services;
4
5
use Illuminate\Database\Eloquent\Model;
6
use Spatie\MediaLibrary\HasMedia\HasMedia;
7
use Terranet\Administrator\Contracts\Services\Saver as SaverContract;
8
use Terranet\Administrator\Field\BelongsTo;
9
use Terranet\Administrator\Field\BelongsToMany;
10
use Terranet\Administrator\Field\Boolean;
11
use Terranet\Administrator\Field\File;
12
use Terranet\Administrator\Field\HasMany;
13
use Terranet\Administrator\Field\HasOne;
14
use Terranet\Administrator\Field\Id;
15
use Terranet\Administrator\Field\Image;
16
use Terranet\Administrator\Field\Media;
17
use Terranet\Administrator\Form\RendersTranslatableElement;
0 ignored issues
show
Bug introduced by
The type Terranet\Administrator\F...dersTranslatableElement was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
18
use Terranet\Administrator\Requests\UpdateRequest;
19
use function admin\db\scheme;
20
21
class Saver implements SaverContract
22
{
23
    /**
24
     * Data collected during saving process.
25
     *
26
     * @var array
27
     */
28
    protected $data = [];
29
30
    /**
31
     * List of relations queued for saving.
32
     *
33
     * @var array
34
     */
35
    protected $relations = [];
36
37
    /**
38
     * Main module repository.
39
     *
40
     * @var Model
41
     */
42
    protected $repository;
43
44
    /**
45
     * @var UpdateRequest
46
     */
47
    protected $request;
48
49
    /**
50
     * Saver constructor.
51
     *
52
     * @param               $eloquent
53
     * @param UpdateRequest $request
54
     */
55
    public function __construct($eloquent, UpdateRequest $request)
56
    {
57
        $this->repository = $eloquent;
58
        $this->request = $request;
59
    }
60
61
    /**
62
     * Process request and persist data.
63
     *
64
     * @return mixed
65
     */
66
    public function sync()
67
    {
68
        $this->connection()->transaction(function () {
69
            foreach ($this->editable() as $field) {
70
                // get original HTML input
71
                $name = $field->id();
72
73
                if ($this->isKey($field)
74
                    || $this->isTranslatable($field)
75
                    || $this->isMediaFile($field)) {
76
                    continue;
77
                }
78
79
                if ($this->isRelation($field)) {
80
                    $this->relations[$name] = $field;
81
                }
82
83
                $value = $this->isFile($field) ? $this->request->file($name) : $this->request->get($name);
84
85
                $value = $this->isBoolean($field) ? (bool) $value : $value;
86
87
                $value = $this->handleJsonType($name, $value);
88
89
                $this->data[$name] = $value;
90
            }
91
92
            $this->cleanData();
93
94
            $this->collectTranslatable();
95
96
            $this->appendTranslationsToRelations();
97
98
            Model::unguard();
99
100
            /*
101
            |-------------------------------------------------------
102
            | Save main data
103
            |-------------------------------------------------------
104
            */
105
            $this->save();
106
107
            /*
108
            |-------------------------------------------------------
109
            | Relationships
110
            |-------------------------------------------------------
111
            | Save related data, fetched by "relation" from related tables
112
            */
113
            $this->saveRelations();
114
115
            $this->saveMedia();
116
117
            Model::reguard();
118
        });
119
120
        return $this->repository;
121
    }
122
123
    /**
124
     * Fetch editable fields.
125
     *
126
     * @return mixed
127
     */
128
    protected function editable()
129
    {
130
        return app('scaffold.form');
131
    }
132
133
    /**
134
     * @param $field
135
     *
136
     * @return bool
137
     */
138
    protected function isKey($field)
139
    {
140
        return $field instanceof Id;
141
    }
142
143
    /**
144
     * @param $field
145
     *
146
     * @return bool
147
     */
148
    protected function isTranslatable($field)
149
    {
150
        return $field instanceof RendersTranslatableElement;
151
    }
152
153
    /**
154
     * @param $field
155
     *
156
     * @return bool
157
     */
158
    protected function isFile($field)
159
    {
160
        return $field instanceof File || $field instanceof Image;
161
    }
162
163
    /**
164
     * Protect request data against external data.
165
     */
166
    protected function cleanData()
167
    {
168
        $this->data = array_except($this->data, [
169
            '_token',
170
            'save',
171
            'save_create',
172
            'save_return',
173
            $this->repository->getKeyName(),
174
        ]);
175
176
        // leave only fillable columns
177
        $this->data = array_only($this->data, $this->repository->getFillable());
178
    }
179
180
    /**
181
     * Persist data.
182
     */
183
    protected function save()
184
    {
185
        $this->nullifyEmptyNullables($this->repository->getTable());
186
187
        $this->repository->fill(
188
            $this->protectAgainstNullPassword($this->data)
0 ignored issues
show
Unused Code introduced by
The call to Terranet\Administrator\S...ctAgainstNullPassword() has too many arguments starting with $this->data. ( Ignorable by Annotation )

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

188
            $this->/** @scrutinizer ignore-call */ 
189
                   protectAgainstNullPassword($this->data)

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...
189
        )->save();
190
    }
191
192
    /**
193
     * Save relations.
194
     */
195
    protected function saveRelations()
196
    {
197
        if (!empty($this->relations)) {
198
            foreach ($this->relations as $name => $field) {
199
                $relation = call_user_func([$this->repository, $name]);
200
201
                switch (get_class($field)) {
202
                    case BelongsTo::class:
203
                        // @var \Illuminate\Database\Eloquent\Relations\BelongsTo $relation
204
                        $relation->associate(
205
                            $this->request->get($relation->getForeignKey())
206
                        );
207
208
                        break;
209
                    case HasOne::class:
210
                        /** @var \Illuminate\Database\Eloquent\Relations\HasOne $relation */
211
                        $related = $relation->getResults();
212
213
                        $related && $related->exists
214
                            ? $relation->update($this->request->get($name))
215
                            : $relation->create($this->request->get($name));
216
217
                        break;
218
                    case BelongsToMany::class:
219
                        $values = array_map('intval', $this->request->get($name, []));
220
                        $relation->sync($values);
221
222
                        break;
223
                    default:
224
                        break;
225
                }
226
            }
227
228
            $this->repository->save();
229
        }
230
    }
231
232
    /**
233
     * Process Media.
234
     */
235
    protected function saveMedia()
236
    {
237
        if ($this->repository instanceof HasMedia) {
238
            $media = (array) $this->request['_media_'];
239
240
            if (!empty($trash = array_get($media, '_trash_', []))) {
241
                $this->repository->media()->whereIn(
242
                    'id',
243
                    $trash
244
                )->delete();
245
            }
246
247
            foreach (array_except($media, '_trash_') as $collection => $objects) {
248
                foreach ($objects as $uploadedFile) {
249
                    $this->repository->addMedia($uploadedFile)->toMediaCollection($collection);
250
                }
251
            }
252
        }
253
    }
254
255
    /**
256
     * Remove null values from data.
257
     *
258
     * @param $relation
259
     * @param $values
260
     *
261
     * @return array
262
     */
263
    protected function forgetNullValues($relation, $values)
264
    {
265
        $keys = explode('.', $this->getQualifiedRelatedKeyName($relation));
0 ignored issues
show
Bug introduced by
The method getQualifiedRelatedKeyName() does not exist on Terranet\Administrator\Services\Saver. ( Ignorable by Annotation )

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

265
        $keys = explode('.', $this->/** @scrutinizer ignore-call */ getQualifiedRelatedKeyName($relation));

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...
266
        $key = array_pop($keys);
267
268
        return array_filter((array) $values[$key], function ($value) {
269
            return null !== $value;
270
        });
271
    }
272
273
    /**
274
     * Collect relations for saving.
275
     *
276
     * @param $field
277
     */
278
    protected function isRelation($field)
279
    {
280
        return ($field instanceof BelongsTo)
281
            || ($field instanceof HasOne)
282
            || ($field instanceof HasMany)
283
            || ($field instanceof BelongsToMany);
284
    }
285
286
    /**
287
     * @param $this
288
     */
289
    protected function appendTranslationsToRelations()
290
    {
291
        if (!empty($this->relations)) {
292
            foreach (array_keys((array) $this->relations) as $relation) {
293
                if ($translations = $this->input("{$relation}.translatable")) {
294
                    $this->relations[$relation] += $translations;
295
                }
296
            }
297
        }
298
    }
299
300
    /**
301
     * @param $name
302
     * @param $value
303
     *
304
     * @return mixed
305
     */
306
    protected function handleJsonType($name, $value)
307
    {
308
        if ($cast = array_get($this->repository->getCasts(), $name)) {
309
            if (in_array($cast, ['array', 'json'], true)) {
310
                $value = json_decode($value);
311
            }
312
        }
313
314
        return $value;
315
    }
316
317
    /**
318
     * Collect translations.
319
     */
320
    protected function collectTranslatable()
321
    {
322
        foreach ($this->request->get('translatable', []) as $key => $value) {
323
            $this->data[$key] = $value;
324
        }
325
    }
326
327
    /**
328
     * Get database connection.
329
     *
330
     * @return \Illuminate\Foundation\Application|mixed
331
     */
332
    protected function connection()
333
    {
334
        return app('db');
335
    }
336
337
    /**
338
     * Retrieve request input value.
339
     *
340
     * @param $key
341
     * @param null $default
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $default is correct as it would always require null to be passed?
Loading history...
342
     *
343
     * @return mixed
344
     */
345
    protected function input($key, $default = null)
346
    {
347
        return app('request')->input($key, $default);
348
    }
349
350
    /**
351
     * Set empty "nullable" values to null.
352
     *
353
     * @param $table
354
     */
355
    protected function nullifyEmptyNullables($table)
356
    {
357
        $columns = scheme()->columns($table);
358
359
        foreach ($this->data as $key => &$value) {
360
            if (!array_key_exists($key, $columns)) {
361
                continue;
362
            }
363
364
            if (!$columns[$key]->getNotnull() && empty($value)) {
365
                $value = null;
366
            }
367
        }
368
    }
369
370
    /**
371
     * Ignore empty password from being saved.
372
     *
373
     * @return array
374
     */
375
    protected function protectAgainstNullPassword(): array
376
    {
377
        if (array_has($this->data, 'password') && empty($this->data['password'])) {
378
            unset($this->data['password']);
379
        }
380
381
        return $this->data;
382
    }
383
384
    /**
385
     * @param $field
386
     *
387
     * @return bool
388
     */
389
    protected function isBoolean($field)
390
    {
391
        return $field instanceof Boolean;
392
    }
393
394
    /**
395
     * @param $field
396
     *
397
     * @return bool
398
     */
399
    protected function isMediaFile($field)
400
    {
401
        return $field instanceof Media;
402
    }
403
}
404