Test Failed
Push — master ( bfaa9c...b480ce )
by Terzi
11:11
created

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

201
                            ? $relation->update(/** @scrutinizer ignore-type */ $this->request->get($name))
Loading history...
202
                            : $relation->create($this->request->get($name));
0 ignored issues
show
Bug introduced by
It seems like $this->request->get($name) can also be of type null; however, parameter $attributes of Illuminate\Database\Eloq...\HasOneOrMany::create() does only seem to accept array, 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

202
                            : $relation->create(/** @scrutinizer ignore-type */ $this->request->get($name));
Loading history...
203
204
                        break;
205
                    case BelongsToMany::class:
206
                        $values = array_map('intval', $this->request->get($name, []));
207
                        $relation->sync($values);
208
209
                        break;
210
                    default:
211
                        break;
212
                }
213
            }
214
215
            $this->repository->save();
216
        }
217
    }
218
219
    /**
220
     * Process Media.
221
     */
222
    protected function saveMedia()
223
    {
224
        if ($this->repository instanceof HasMedia) {
225
            $media = (array) $this->request['_media_'];
226
227
            if (!empty($trash = array_get($media, '_trash_', []))) {
228
                $this->repository->media()->whereIn(
229
                    'id',
230
                    $trash
231
                )->delete();
232
            }
233
234
            foreach (array_except($media, '_trash_') as $collection => $objects) {
235
                foreach ($objects as $uploadedFile) {
236
                    $this->repository->addMedia($uploadedFile)->toMediaCollection($collection);
237
                }
238
            }
239
        }
240
    }
241
242
    /**
243
     * Remove null values from data.
244
     *
245
     * @param $relation
246
     * @param $values
247
     *
248
     * @return array
249
     */
250
    protected function forgetNullValues($relation, $values)
251
    {
252
        $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

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