Completed
Push — master ( 076f7e...a7d859 )
by Song
02:39
created

MultipleFile::render()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 17
rs 9.7
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 (request()->has(static::FILE_SORT_FLAG)) {
67
            return false;
68
        }
69
70
        if ($this->validator) {
71
            return $this->validator->call($this, $input);
72
        }
73
74
        $attributes = [];
75
76
        if (!$fieldRules = $this->getRules()) {
77
            return false;
78
        }
79
80
        $attributes[$this->column] = $this->label;
81
82
        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...
83
84
        return \validator($input, $rules, $this->getValidationMessages(), $attributes);
85
    }
86
87
    /**
88
     * Hydrate the files array.
89
     *
90
     * @param array $value
91
     *
92
     * @return array
93
     */
94
    protected function hydrateFiles(array $value)
95
    {
96
        if (empty($value)) {
97
            return [[$this->column => $this->getRules()], []];
98
        }
99
100
        $rules = $input = [];
101
102
        foreach ($value as $key => $file) {
103
            $rules[$this->column.$key] = $this->getRules();
104
            $input[$this->column.$key] = $file;
105
        }
106
107
        return [$rules, $input];
108
    }
109
110
    /**
111
     * Sort files.
112
     *
113
     * @param string $order
114
     *
115
     * @return array
116
     */
117
    protected function sortFiles($order)
118
    {
119
        $order = explode(',', $order);
120
121
        $new = [];
122
        $original = $this->original();
123
124
        foreach ($order as $item) {
125
            $new[] = Arr::get($original, $item);
126
        }
127
128
        return $new;
129
    }
130
131
    /**
132
     * Prepare for saving.
133
     *
134
     * @param UploadedFile|array $files
135
     *
136
     * @return mixed|string
137
     */
138
    public function prepare($files)
139
    {
140
        if (request()->has(static::FILE_DELETE_FLAG)) {
141
142
            if ($this->pathColumn) {
143
                return $this->destroyFromHasMany(request(static::FILE_DELETE_FLAG));
144
            }
145
146
            return $this->destroy(request(static::FILE_DELETE_FLAG));
147
        }
148
149
        if (is_string($files) && request()->has(static::FILE_SORT_FLAG)) {
150
            return $this->sortFiles($files);
151
        }
152
153
        $targets = array_map([$this, 'prepareForeach'], $files);
154
155
        // for create or update
156
        if ($this->pathColumn) {
157
            $targets = array_map(function ($target) {
158
                return [$this->pathColumn => $target];
159
            }, $targets);
160
        }
161
162
        return array_merge($this->original(), $targets);
163
    }
164
165
    /**
166
     * @return array|mixed
167
     */
168
    public function original()
169
    {
170
        if (empty($this->original)) {
171
            return [];
172
        }
173
174
        return $this->original;
175
    }
176
177
    /**
178
     * Prepare for each file.
179
     *
180
     * @param UploadedFile $file
181
     *
182
     * @return mixed|string
183
     */
184 View Code Duplication
    protected function prepareForeach(UploadedFile $file = null)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
185
    {
186
        $this->name = $this->getStoreName($file);
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 184 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...
187
188
        return tap($this->upload($file), function () {
0 ignored issues
show
Bug introduced by
It seems like $file defined by parameter $file on line 184 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...
189
            $this->name = null;
190
        });
191
    }
192
193
    /**
194
     * Preview html for file-upload plugin.
195
     *
196
     * @return array
197
     */
198
    protected function preview()
199
    {
200
        $files = $this->value ?: [];
201
202
        return array_values(array_map([$this, 'objectUrl'], $files));
203
    }
204
205
    /**
206
     * Initialize the caption.
207
     *
208
     * @param array $caption
209
     *
210
     * @return string
211
     */
212
    protected function initialCaption($caption)
213
    {
214
        if (empty($caption)) {
215
            return '';
216
        }
217
218
        $caption = array_map('basename', $caption);
219
220
        return implode(',', $caption);
221
    }
222
223
    /**
224
     * @return array
225
     */
226
    protected function initialPreviewConfig()
227
    {
228
        $files = $this->value ?: [];
229
230
        $config = [];
231
232
        foreach ($files as $index => $file) {
233
234
            if (is_array($file) && $this->pathColumn) {
235
                $index = Arr::get($file, $this->getRelatedKeyName(), $index);
236
                $file = Arr::get($file, $this->pathColumn);
237
            }
238
239
            $preview = array_merge([
240
                'caption' => basename($file),
241
                'key'     => $index,
242
            ], $this->guessPreviewType($file));
243
244
            $config[] = $preview;
245
        }
246
247
        return $config;
248
    }
249
250
    /**
251
     * Get related model key name.
252
     *
253
     * @return string
254
     */
255
    protected function getRelatedKeyName()
256
    {
257
        if (is_null($this->form)) {
258
            return;
259
        }
260
261
        return $this->form->model()->{$this->column}()->getRelated()->getKeyName();
262
    }
263
264
    /**
265
     * Allow to sort files.
266
     *
267
     * @return $this
268
     */
269
    public function sortable()
270
    {
271
        $this->fileActionSettings['showDrag'] = true;
272
273
        return $this;
274
    }
275
276
    /**
277
     * @param string $options
278
     */
279
    protected function setupScripts($options)
280
    {
281
        $this->script = <<<EOT
282
$("input{$this->getElementClassSelector()}").fileinput({$options});
283
EOT;
284
285 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...
286
            $text = [
287
                'title'   => trans('admin.delete_confirm'),
288
                'confirm' => trans('admin.confirm'),
289
                'cancel'  => trans('admin.cancel'),
290
            ];
291
292
            $this->script .= <<<EOT
293
$("input{$this->getElementClassSelector()}").on('filebeforedelete', function() {
294
295
    return new Promise(function(resolve, reject) {
296
297
        var remove = resolve;
298
299
        swal({
300
            title: "{$text['title']}",
301
            type: "warning",
302
            showCancelButton: true,
303
            confirmButtonColor: "#DD6B55",
304
            confirmButtonText: "{$text['confirm']}",
305
            showLoaderOnConfirm: true,
306
            cancelButtonText: "{$text['cancel']}",
307
            preConfirm: function() {
308
                return new Promise(function(resolve) {
309
                    resolve(remove());
310
                });
311
            }
312
        });
313
    });
314
});
315
EOT;
316
        }
317
318
        if ($this->fileActionSettings['showDrag']) {
319
            $this->addVariables([
320
                'sortable'  => true,
321
                'sort_flag' => static::FILE_SORT_FLAG,
322
            ]);
323
324
            $this->script .= <<<EOT
325
$("input{$this->getElementClassSelector()}").on('filesorted', function(event, params) {
326
327
    var order = [];
328
329
    params.stack.forEach(function (item) {
330
        order.push(item.key);
331
    });
332
333
    $("input{$this->getElementClassSelector()}_sort").val(order);
334
});
335
EOT;
336
        }
337
    }
338
339
    /**
340
     * Render file upload field.
341
     *
342
     * @return \Illuminate\Contracts\View\Factory|\Illuminate\View\View
343
     */
344
    public function render()
345
    {
346
        $this->attribute('multiple', true);
347
348
        $this->setupDefaultOptions();
349
350
        if (!empty($this->value)) {
351
            $this->options(['initialPreview' => $this->preview()]);
352
            $this->setupPreviewOptions();
353
        }
354
355
        $options = json_encode($this->options);
356
357
        $this->setupScripts($options);
358
359
        return parent::render();
0 ignored issues
show
Bug Compatibility introduced by
The expression parent::render(); of type string|Illuminate\View\V...\Contracts\View\Factory adds the type Illuminate\Contracts\View\Factory to the return on line 359 which is incompatible with the return type declared by the interface Illuminate\Contracts\Support\Renderable::render of type string.
Loading history...
360
    }
361
362
    /**
363
     * Destroy original files.
364
     *
365
     * @param string $key
366
     *
367
     * @return array
368
     */
369
    public function destroy($key)
370
    {
371
        $files = $this->original ?: [];
372
373
        $path = Arr::get($files, $key);
374
375
        if (!$this->retainable && $this->storage->exists($path)) {
376
            $this->storage->delete($path);
377
        }
378
379
        unset($files[$key]);
380
381
        return $files;
382
    }
383
384
    /**
385
     * Destroy original files from hasmany related model.
386
     *
387
     * @param integer $key
388
     * @return array
389
     */
390
    public function destroyFromHasMany($key)
391
    {
392
        $files = collect($this->original ?: [])->keyBy($this->getRelatedKeyName())->toArray();
393
394
        $path = Arr::get($files, "{$key}.{$this->pathColumn}");
395
396
        if (!$this->retainable && $this->storage->exists($path)) {
397
            $this->storage->delete($path);
398
        }
399
400
        $files[$key][Form::REMOVE_FLAG_NAME] = 1;
401
402
        return $files;
403
    }
404
}
405