Saver::isMediaFile()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 1
dl 0
loc 3
ccs 0
cts 0
cp 0
crap 2
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Terranet\Administrator\Services;
4
5
use Illuminate\Database\Eloquent\Model;
0 ignored issues
show
Bug introduced by
The type Illuminate\Database\Eloquent\Model 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...
6
use Illuminate\Support\Arr;
7
use Spatie\MediaLibrary\HasMedia\HasMedia;
0 ignored issues
show
Bug introduced by
The type Spatie\MediaLibrary\HasMedia\HasMedia 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...
8
use Terranet\Administrator\Contracts\Services\Saver as SaverContract;
9
use Terranet\Administrator\Field\BelongsTo;
10
use Terranet\Administrator\Field\BelongsToMany;
11
use Terranet\Administrator\Field\Boolean;
12
use Terranet\Administrator\Field\File;
13
use Terranet\Administrator\Field\HasMany;
14
use Terranet\Administrator\Field\HasOne;
15
use Terranet\Administrator\Field\Id;
16
use Terranet\Administrator\Field\Image;
17
use Terranet\Administrator\Field\Media;
18
use Terranet\Administrator\Field\Translatable;
19
use Terranet\Administrator\Field\Traits\HandlesRelation;
20
use Terranet\Administrator\Requests\UpdateRequest;
21
use function admin\db\scheme;
22
23
class Saver implements SaverContract
24
{
25
    use HandlesRelation;
0 ignored issues
show
introduced by
The trait Terranet\Administrator\F...\Traits\HandlesRelation requires some properties which are not provided by Terranet\Administrator\Services\Saver: $model, $id
Loading history...
26
27
    /**
28
     * Data collected during saving process.
29
     *
30
     * @var array
31
     */
32
    protected $data = [];
33
34
    /**
35
     * List of relations queued for saving.
36
     *
37
     * @var array
38
     */
39
    protected $relations = [];
40
41
    /**
42
     * Main module repository.
43
     *
44
     * @var Model
45
     */
46
    protected $repository;
47
48
    /**
49
     * @var UpdateRequest
50
     */
51
    protected $request;
52
53
    /**
54
     * Saver constructor.
55
     *
56
     * @param               $eloquent
57
     * @param  UpdateRequest  $request
58
     */
59
    public function __construct($eloquent, UpdateRequest $request)
60
    {
61
        $this->repository = $eloquent;
62
        $this->request = $request;
63
    }
64
65
    /**
66
     * Process request and persist data.
67
     *
68
     * @return mixed
69
     */
70
    public function sync()
71
    {
72
        $this->connection()->transaction(function () {
73
            foreach ($this->editable() as $field) {
74
                // get original HTML input
75
                $name = $field->id();
76
77
                if ($this->isKey($field) || $this->isMediaFile($field) || $this->isTranslatable($field)) {
78
                    continue;
79
                }
80
81
                if ($this->isRelation($field)) {
82
                    $this->relations[$name] = $field;
83
                }
84
85
                $value = $this->isFile($field) ? $this->request->file($name) : $this->request->get($name);
86
87
                $value = $this->isBoolean($field) ? (bool) $value : $value;
88
89
                $value = $this->handleJsonType($name, $value);
90
91
                $this->data[$name] = $value;
92
            }
93
94
            $this->cleanData();
95
96
            $this->collectTranslatable();
97
98
            $this->appendTranslationsToRelations();
99
100
            Model::unguard();
101
102
            $this->process();
103
104
            Model::reguard();
105
        });
106
107
        return $this->repository;
108
    }
109
110
    /**
111
     * Fetch editable fields.
112
     *
113
     * @return mixed
114
     */
115
    protected function editable()
116
    {
117
        return app('scaffold.module')->form();
0 ignored issues
show
Bug introduced by
The function app was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

117
        return /** @scrutinizer ignore-call */ app('scaffold.module')->form();
Loading history...
118
    }
119
120
    /**
121
     * @param $field
122
     * @return bool
123
     */
124
    protected function isKey($field)
125
    {
126
        return $field instanceof Id;
127
    }
128
129
    /**
130
     * @param $field
131
     * @return bool
132
     */
133
    protected function isFile($field)
134
    {
135
        return $field instanceof File || $field instanceof Image;
136
    }
137
138
    /**
139
     * Protect request data against external data.
140
     */
141
    protected function cleanData()
142
    {
143
        $this->data = Arr::except($this->data, [
144
            '_token',
145
            'save',
146
            'save_create',
147
            'save_return',
148
            $this->repository->getKeyName(),
149
        ]);
150
151
        // clean from relationable fields.
152
        $this->data = array_diff_key($this->data, $this->relations);
153
    }
154
155
    /**
156
     * Persist main eloquent model including relations & media.
157
     */
158
    protected function process()
159
    {
160
        $this->nullifyEmptyNullables($this->repository->getTable());
161
162
        \DB::transaction(function () {
163
            $this->repository->fill(
164
                $this->excludeUnfillable(
165
                    $this->protectAgainstNullPassword()
166
                )
167
            )->save();
168
169
            $this->saveRelations();
170
171
            $this->saveMedia();
172
        });
173
    }
174
175
    /**
176
     * Save relations.
177
     */
178
    protected function saveRelations()
179
    {
180
        foreach ($this->relations as $name => $field) {
181
            $relation = \call_user_func([$this->repository, $name]);
182
183
            switch (\get_class($field)) {
184
                case BelongsTo::class:
185
                    /** @var \Illuminate\Database\Eloquent\Relations\BelongsTo $relation */
186
                    $relation->associate(
187
                        $this->request->get($this->getForeignKey($relation))
188
                    )->save();
189
190
                    break;
191
                case HasOne::class:
192
                    /** @var \Illuminate\Database\Eloquent\Relations\HasOne $relation */
193
                    $related = $relation->getResults();
194
195
                    $related && $related->exists
196
                        ? $related->update($this->request->get($name))
197
                        : $this->repository->{$name}()->create($this->request->get($name));
198
199
                    break;
200
                case BelongsToMany::class:
201
                    $values = array_map('intval', $this->request->get($name, []));
202
                    $relation->sync($values);
203
204
                    break;
205
                default:
206
                    break;
207
            }
208
        }
209
    }
210
211
    /**
212
     * Process Media.
213
     */
214
    protected function saveMedia()
215
    {
216
        if ($this->repository instanceof HasMedia) {
217
            $media = (array) $this->request['_media_'];
218
219
            if (!empty($trash = Arr::get($media, '_trash_', []))) {
220
                $this->repository->media()->whereIn(
221
                    'id',
222
                    $trash
223
                )->delete();
224
            }
225
226
            foreach (Arr::except($media, '_trash_') as $collection => $objects) {
227
                foreach ($objects as $uploadedFile) {
228
                    $this->repository->addMedia($uploadedFile)->toMediaCollection($collection);
229
                }
230
            }
231
        }
232
    }
233
234
    /**
235
     * Remove null values from data.
236
     *
237
     * @param $relation
238
     * @param $values
239
     * @return array
240
     */
241
    protected function forgetNullValues($relation, $values)
242
    {
243
        $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

243
        $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...
244
        $key = array_pop($keys);
245
246
        return array_filter((array) $values[$key], function ($value) {
247
            return null !== $value;
248
        });
249
    }
250
251
    /**
252
     * Collect relations for saving.
253
     *
254
     * @param $field
255
     */
256
    protected function isRelation($field)
257
    {
258
        return ($field instanceof BelongsTo)
259
            || ($field instanceof HasOne)
260
            || ($field instanceof HasMany)
261
            || ($field instanceof BelongsToMany);
262
    }
263
264
    /**
265
     * @param $this
266
     */
267
    protected function appendTranslationsToRelations()
268
    {
269
        if (!empty($this->relations)) {
270
            foreach (array_keys((array) $this->relations) as $relation) {
271
                if ($translations = $this->input("{$relation}.translatable")) {
272
                    $this->relations[$relation] += $translations;
273
                }
274
            }
275
        }
276
    }
277
278
    /**
279
     * @param $name
280
     * @param $value
281
     * @return mixed
282
     */
283
    protected function handleJsonType($name, $value)
284
    {
285
        if ($cast = Arr::get($this->repository->getCasts(), $name)) {
286
            if (\in_array($cast, ['array', 'json'], true)) {
287
                $value = json_decode($value);
288
            }
289
        }
290
291
        return $value;
292
    }
293
294
    /**
295
     * Collect translations.
296
     */
297
    protected function collectTranslatable()
298
    {
299
        foreach ($this->request->get('translatable', []) as $key => $value) {
300
            $this->data[$key] = $value;
301
        }
302
    }
303
304
    /**
305
     * Get database connection.
306
     *
307
     * @return \Illuminate\Foundation\Application|mixed
0 ignored issues
show
Bug introduced by
The type Illuminate\Foundation\Application 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...
308
     */
309
    protected function connection()
310
    {
311
        return app('db');
0 ignored issues
show
Bug introduced by
The function app was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

311
        return /** @scrutinizer ignore-call */ app('db');
Loading history...
312
    }
313
314
    /**
315
     * Retrieve request input value.
316
     *
317
     * @param $key
318
     * @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...
319
     * @return mixed
320
     */
321
    protected function input($key, $default = null)
322
    {
323
        return app('request')->input($key, $default);
0 ignored issues
show
Bug introduced by
The function app was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

323
        return /** @scrutinizer ignore-call */ app('request')->input($key, $default);
Loading history...
324
    }
325
326
    /**
327
     * Set empty "nullable" values to null.
328
     *
329
     * @param $table
330
     */
331
    protected function nullifyEmptyNullables($table)
332
    {
333
        $columns = scheme()->columns($table);
334
335
        foreach ($this->data as $key => &$value) {
336
            if (!array_key_exists($key, $columns)) {
337
                continue;
338
            }
339
340
            if (!$columns[$key]->getNotnull() && empty($value)) {
341
                $value = null;
342
            }
343
        }
344
    }
345
346
    /**
347
     * Exclude unfillable columns.
348
     *
349
     * @param  array  $data
350
     * @return array
351
     */
352
    protected function excludeUnfillable(array $data): array
353
    {
354
        $fillable = array_merge(
355
            $this->repository->getFillable(),
356
            scheme()->columns($this->repository->getTable())
357
        );
358
359
        foreach ($data as $key => $value) {
360
            /**
361
             * @note `is_string` verification is required to filter only string-based keys,
362
             * but leave `translatable` keys untouched
363
             */
364
            if (is_string($key) && !in_array($key, $fillable)) {
365
                unset($data[$key]);
366
            }
367
        }
368
369
        return $data;
370
    }
371
372
    /**
373
     * Ignore empty password from being saved.
374
     *
375
     * @return array
376
     */
377
    protected function protectAgainstNullPassword(): array
378
    {
379
        if (Arr::has($this->data, 'password') && empty($this->data['password'])) {
380
            unset($this->data['password']);
381
        }
382
383
        return $this->data;
384
    }
385
386
    /**
387
     * @param $field
388
     * @return bool
389
     */
390
    protected function isBoolean($field)
391
    {
392
        return $field instanceof Boolean;
393
    }
394
395
    /**
396
     * @param $field
397
     * @return bool
398
     */
399
    protected function isMediaFile($field)
400
    {
401
        return $field instanceof Media;
402
    }
403
404
    /**
405
     * @param $field
406
     * @return bool
407
     */
408
    protected function isTranslatable($field)
409
    {
410
        return $field instanceof Translatable;
411
    }
412
}
413