Completed
Push — master ( 9b49f7...0d2bac )
by Song
03:00 queued 10s
created

MultipleFile::destroy()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
nc 6
nop 1
dl 0
loc 22
rs 8.6346
c 0
b 0
f 0
1
<?php
2
3
namespace Encore\Admin\Form\Field;
4
5
use Encore\Admin\Form;
6
use Encore\Admin\Form\Field;
7
use Illuminate\Support\Arr;
8
use Symfony\Component\HttpFoundation\File\UploadedFile;
9
10
class MultipleFile extends Field
11
{
12
    use UploadField;
13
14
    /**
15
     * Css.
16
     *
17
     * @var array
18
     */
19
    protected static $css = [
20
        '/vendor/laravel-admin/bootstrap-fileinput/css/fileinput.min.css?v=4.5.2',
21
    ];
22
23
    /**
24
     * Js.
25
     *
26
     * @var array
27
     */
28
    protected static $js = [
29
        '/vendor/laravel-admin/bootstrap-fileinput/js/plugins/canvas-to-blob.min.js',
30
        '/vendor/laravel-admin/bootstrap-fileinput/js/fileinput.min.js?v=4.5.2',
31
        '/vendor/laravel-admin/bootstrap-fileinput/js/plugins/sortable.min.js?v=4.5.2',
32
    ];
33
34
    /**
35
     * Create a new File instance.
36
     *
37
     * @param string $column
38
     * @param array  $arguments
39
     */
40
    public function __construct($column, $arguments = [])
41
    {
42
        $this->initStorage();
43
44
        parent::__construct($column, $arguments);
45
    }
46
47
    /**
48
     * Default directory for file to upload.
49
     *
50
     * @return mixed
51
     */
52
    public function defaultDirectory()
53
    {
54
        return config('admin.upload.directory.file');
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function getValidator(array $input)
61
    {
62
        if (request()->has(static::FILE_DELETE_FLAG)) {
63
            return false;
64
        }
65
66
        if ($this->validator) {
67
            return $this->validator->call($this, $input);
68
        }
69
70
        $attributes = [];
71
72
        if (!$fieldRules = $this->getRules()) {
73
            return false;
74
        }
75
76
        $attributes[$this->column] = $this->label;
77
78
        list($rules, $input) = $this->hydrateFiles(Arr::get($input, $this->column, []));
0 ignored issues
show
Bug introduced by
It seems like $this->column can also be of type array; however, Illuminate\Support\Arr::get() does only seem to accept string|integer|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
79
80
        return \validator($input, $rules, $this->getValidationMessages(), $attributes);
81
    }
82
83
    /**
84
     * Hydrate the files array.
85
     *
86
     * @param array $value
87
     *
88
     * @return array
89
     */
90
    protected function hydrateFiles(array $value)
91
    {
92
        if (empty($value)) {
93
            return [[$this->column => $this->getRules()], []];
94
        }
95
96
        $rules = $input = [];
97
98
        foreach ($value as $key => $file) {
99
            $rules[$this->column . $key] = $this->getRules();
100
            $input[$this->column . $key] = $file;
101
        }
102
103
        return [$rules, $input];
104
    }
105
106
    /**
107
     * Sort files.
108
     *
109
     * @param string $order
110
     *
111
     * @return array
112
     */
113
    protected function sortFiles($order)
114
    {
115
        $order = explode(',', $order);
116
117
        $new = [];
118
        $original = $this->original();
119
120
        foreach ($order as $item) {
121
            $new[] = Arr::get($original, $item);
122
        }
123
124
        return $new;
125
    }
126
127
    /**
128
     * Prepare for saving.
129
     *
130
     * @param UploadedFile|array $files
131
     *
132
     * @return mixed|string
133
     */
134
    public function prepare($files)
135
    {
136
        if (request()->has(static::FILE_DELETE_FLAG)) {
137
            if ($this->pathColumn) {
138
                return $this->destroyFromHasMany(request(static::FILE_DELETE_FLAG));
139
            }
140
141
            return $this->destroy(request(static::FILE_DELETE_FLAG));
142
        }
143
144
        if (is_string($files) && request()->has(static::FILE_SORT_FLAG)) {
145
            return $this->sortFiles($files);
146
        }
147
148
        $targets = array_map([$this, 'prepareForeach'], $files);
149
150
        // for create or update
151
        if ($this->pathColumn) {
152
            $targets = array_map(function ($target) {
153
                return [$this->pathColumn => $target];
154
            }, $targets);
155
        }
156
157
        return array_merge($this->original(), $targets);
158
    }
159
160
    /**
161
     * @return array|mixed
162
     */
163
    public function original()
164
    {
165
        if (empty($this->original)) {
166
            return [];
167
        }
168
169
        return $this->original;
170
    }
171
172
    /**
173
     * Prepare for each file.
174
     *
175
     * @param UploadedFile $file
176
     *
177
     * @return mixed|string
178
     */
179
    protected function prepareForeach(UploadedFile $file = null)
180
    {
181
        $this->name = $this->getStoreName($file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 179 can be null; however, Encore\Admin\Form\Field\...adField::getStoreName() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
Documentation Bug introduced by
It seems like $this->getStoreName($file) of type string is incompatible with the declared type null of property $name.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
182
183
        return tap($this->upload($file), function () {
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 179 can be null; however, Encore\Admin\Form\Field\UploadField::upload() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
184
            $this->name = null;
185
        });
186
    }
187
188
    /**
189
     * Preview html for file-upload plugin.
190
     *
191
     * @return array
192
     */
193
    protected function preview()
194
    {
195
        $files = $this->value ?: [];
196
197
        return array_values(array_map([$this, 'objectUrl'], $files));
198
    }
199
200
    /**
201
     * Initialize the caption.
202
     *
203
     * @param array $caption
204
     *
205
     * @return string
206
     */
207
    protected function initialCaption($caption)
208
    {
209
        if (empty($caption)) {
210
            return '';
211
        }
212
213
        $caption = array_map('basename', $caption);
214
215
        return implode(',', $caption);
216
    }
217
218
    /**
219
     * @return array
220
     */
221
    protected function initialPreviewConfig()
222
    {
223
        $files = $this->value ?: [];
224
225
        $config = [];
226
227
        foreach ($files as $index => $file) {
228
            if (is_array($file) && $this->pathColumn) {
229
                $index = Arr::get($file, $this->getRelatedKeyName(), $index);
230
                $file = Arr::get($file, $this->pathColumn);
231
            }
232
233
            $preview = array_merge([
234
                'caption' => basename($file),
235
                'key'     => $index,
236
            ], $this->guessPreviewType($file));
237
238
            $config[] = $preview;
239
        }
240
241
        return $config;
242
    }
243
244
    /**
245
     * Get related model key name.
246
     *
247
     * @return string
248
     */
249
    protected function getRelatedKeyName()
250
    {
251
        if (is_null($this->form)) {
252
            return;
253
        }
254
255
        return $this->form->model()->{$this->column}()->getRelated()->getKeyName();
256
    }
257
258
    /**
259
     * Allow to sort files.
260
     *
261
     * @return $this
262
     */
263
    public function sortable()
264
    {
265
        $this->fileActionSettings['showDrag'] = true;
266
267
        return $this;
268
    }
269
270
    /**
271
     * @param string $options
272
     */
273
    protected function setupScripts($options)
274
    {
275
        $this->script = <<<EOT
276
$("input{$this->getElementClassSelector()}").fileinput({$options});
277
EOT;
278
279 View Code Duplication
        if ($this->fileActionSettings['showRemove']) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
280
            $text = [
281
                'title'   => trans('admin.delete_confirm'),
282
                'confirm' => trans('admin.confirm'),
283
                'cancel'  => trans('admin.cancel'),
284
            ];
285
286
            $this->script .= <<<EOT
287
$("input{$this->getElementClassSelector()}").on('filebeforedelete', function() {
288
289
    return new Promise(function(resolve, reject) {
290
291
        var remove = resolve;
292
293
        swal({
294
            title: "{$text['title']}",
295
            type: "warning",
296
            showCancelButton: true,
297
            confirmButtonColor: "#DD6B55",
298
            confirmButtonText: "{$text['confirm']}",
299
            showLoaderOnConfirm: true,
300
            cancelButtonText: "{$text['cancel']}",
301
            preConfirm: function() {
302
                return new Promise(function(resolve) {
303
                    resolve(remove());
304
                });
305
            }
306
        });
307
    });
308
});
309
EOT;
310
        }
311
312
        if ($this->fileActionSettings['showDrag']) {
313
            $this->addVariables([
314
                'sortable'  => true,
315
                'sort_flag' => static::FILE_SORT_FLAG,
316
            ]);
317
318
            $this->script .= <<<EOT
319
$("input{$this->getElementClassSelector()}").on('filesorted', function(event, params) {
320
321
    var order = [];
322
323
    params.stack.forEach(function (item) {
324
        order.push(item.key);
325
    });
326
327
    $("input{$this->getElementClassSelector()}_sort").val(order);
328
});
329
EOT;
330
        }
331
    }
332
333
    /**
334
     * Render file upload field.
335
     *
336
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
337
     */
338
    public function render()
339
    {
340
        $this->attribute('multiple', true);
341
342
        $this->setupDefaultOptions();
343
344
        if (!empty($this->value)) {
345
            $this->options(['initialPreview' => $this->preview()]);
346
            $this->setupPreviewOptions();
347
        }
348
349
        $options = json_encode($this->options);
350
351
        $this->setupScripts($options);
352
353
        return parent::render();
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::render(); of type Illuminate\Contracts\Vie...minate\View\View|string adds the type Illuminate\Contracts\View\Factory to the return on line 353 which is incompatible with the return type declared by the interface Illuminate\Contracts\Support\Renderable::render of type string.
Loading history...
354
    }
355
356
    /**
357
     * Destroy original files.
358
     *
359
     * @param string $key
360
     *
361
     * @return array
362
     */
363
    public function destroy($key)
364
    {
365
        $files = $this->original ?: [];
366
367
        $path = Arr::get($files, $key);
368
369
        if (!$this->retainable && $this->storage->exists($path)) {
370
            /* If this field class is using ImageField trait i.e MultipleImage field, 
371
            we loop through the thumbnails to delete them as well. */
372
373
            if (isset($this->thumbnails) && method_exists($this, 'destroyThumbnailFile')) {
374
                foreach ($this->thumbnails as $name => $_) {
0 ignored issues
show
Bug introduced by
The property thumbnails does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
375
                    $this->destroyThumbnailFile($path, $name);
0 ignored issues
show
Documentation Bug introduced by
The method destroyThumbnailFile does not exist on object<Encore\Admin\Form\Field\MultipleFile>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
376
                }
377
            }
378
            $this->storage->delete($path);
379
        }
380
381
        unset($files[$key]);
382
383
        return $files;
384
    }
385
386
    /**
387
     * Destroy original files from hasmany related model.
388
     *
389
     * @param int $key
390
     *
391
     * @return array
392
     */
393
    public function destroyFromHasMany($key)
394
    {
395
        $files = collect($this->original ?: [])->keyBy($this->getRelatedKeyName())->toArray();
396
397
        $path = Arr::get($files, "{$key}.{$this->pathColumn}");
398
399
        if (!$this->retainable && $this->storage->exists($path)) {
400
            $this->storage->delete($path);
401
        }
402
403
        $files[$key][Form::REMOVE_FLAG_NAME] = 1;
404
405
        return $files;
406
    }
407
}
408