Completed
Pull Request — master (#4)
by
unknown
01:31
created

FileBehavior::filePresetAfterUpload()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 0
cts 5
cp 0
rs 10
c 0
b 0
f 0
cc 3
nc 2
nop 1
crap 12
1
<?php
2
3
/**
4
 * @link https://github.com/rkit/filemanager-yii2
5
 * @copyright Copyright (c) 2015 Igor Romanov
6
 * @license [MIT](http://opensource.org/licenses/MIT)
7
 */
8
9
namespace rkit\filemanager\behaviors;
10
11
use rkit\filemanager\models\FileUploadSession;
12
use Yii;
13
use yii\base\Behavior;
14
use yii\db\ActiveRecord;
15
use yii\helpers\ArrayHelper;
16
17
class FileBehavior extends Behavior
18
{
19
    /**
20
     * @var array
21
     */
22
    public $attributes = [];
23
    /**
24
     * @var ActiveQuery
25
     */
26
    private $relation;
27
    /**
28
     * @var FileBind
29
     */
30
    private $fileBind;
31
32
    /**
33
     * @internal
34
     */
35 31
    public function init()
36
    {
37 31
        parent::init();
38
39 31
        $this->fileBind = new FileBind();
40
41 31
        Yii::$app->fileManager->registerTranslations();
42 31
    }
43
44
    /**
45
     * @inheritdoc
46
     * @internal
47
     */
48 31
    public function events()
49
    {
50
        return [
51 31
            ActiveRecord::EVENT_AFTER_INSERT  => 'afterSave',
52 31
            ActiveRecord::EVENT_AFTER_UPDATE  => 'afterSave',
53 31
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
54 31
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
55 31
            ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
56
        ];
57
    }
58
59
    /**
60
     * @internal
61
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
62
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
63
     */
64 2
    public function beforeSave($insert)
0 ignored issues
show
Unused Code introduced by
The parameter $insert is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
65
    {
66 2
        foreach ($this->attributes as $attribute => $options) {
67 2
            $oldValue = $this->owner->isNewRecord ? null : $this->owner->getOldAttribute($attribute);
68 2
            $isAttributeChanged = $oldValue === null ? true : $this->owner->isAttributeChanged($attribute);
69
70 2
            $this->attributes[$attribute]['isAttributeChanged'] = $isAttributeChanged;
71 2
            $this->attributes[$attribute]['oldValue'] = $oldValue;
72
        }
73 2
    }
74
75
    /**
76
     * @internal
77
     */
78 2
    public function afterSave()
79
    {
80 2
        foreach ($this->attributes as $attribute => $options) {
81 2
            $files = $this->owner->{$attribute};
82
83 2
            $isAttributeNotChanged = $options['isAttributeChanged'] === false || $files === null;
84 2
            if ($isAttributeNotChanged) {
85 2
                continue;
86
            }
87
88 2
            if (is_numeric($files)) {
89
                $files = [$files];
90
            }
91
92 2
            if (is_array($files)) {
93 1
                $files = array_filter($files);
94
            }
95
96 2
            if ($files === [] || $files === '') {
97 1
                $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
98 1
                continue;
99
            }
100
101 1
            $maxFiles = ArrayHelper::getValue($this->fileRules($attribute, true), 'maxFiles');
102 1
            if (is_array($files) && $maxFiles !== null) {
103 1
                $files = array_slice($files, 0, $maxFiles, true);
104
            }
105
106 1
            $files = $this->fileBind->bind($this->owner, $attribute, $files);
107 1
            if (is_array($files)) {
108
                $files = array_shift($files);
109
            }
110
111 1
            $this->clearState($attribute, $files);
112
            $this->setValue($attribute, $files, $options['oldValue']);
113
        }
114 1
    }
115
116
    /**
117
     * @internal
118
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
119
     */
120
    public function beforeDelete()
121
    {
122
        foreach ($this->attributes as $attribute => $options) {
123
            $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
124
        }
125
    }
126
127 1
    public function clearState($attribute, $files)
128
    {
129 1
        if (!is_array($files)) {
130 1
            $files = [$files];
131
        }
132
        $query = [
133 1
            'created_user_id' => Yii::$app->user->id,
134 1
            'target_model_class' => get_class($this->owner),
135 1
            'target_model_id' => $this->owner->getPrimaryKey(),
136 1
            'target_model_attribute' => $attribute,
137
        ];
138 1
        if ($files) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $files of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
139 1
            $fileIDs = ArrayHelper::getColumn($files, 'id');
140 1
            $query['file_id'] = $fileIDs;
141
        }
142 1
        FileUploadSession::deleteAll($query);
143
        $query['target_model_id'] = null;
144
        FileUploadSession::deleteAll($query);  // for cases of uploads when original model was a new record at the moment of uploads
145
        return;
146
    }
147
148 25
    private function setState($attribute, $file)
149
    {
150 25
        $rec = new FileUploadSession();
151 25
        $rec->created_user_id = Yii::$app->user->id;
152
        $rec->file_id = $file->getPrimaryKey();
153
        $rec->target_model_attribute = $attribute; // TODO: write model/object id?
154
        $rec->target_model_id = (!$this->owner->isNewRecord ? $this->owner->getPrimaryKey() : null);
155
        $rec->target_model_class = get_class($this->owner);
156
        $rec->save(false);
157
    }
158
159
    private function setValue($attribute, $file, $defaultValue)
160
    {
161
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
162
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
163
164
        if ($saveFilePath || $saveFileId) {
165
            if (!$file) {
166
                $value = $defaultValue;
167
            } elseif ($saveFilePath) {
168
                $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
169
                $value = Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $handlerTemplatePath($file);
170
            } elseif ($saveFileId) {
171
                $value = $file->getPrimaryKey();
172
            }
173
            $this->owner->{$attribute} = $value;
0 ignored issues
show
Bug introduced by
The variable $value does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
174
            $this->owner->updateAttributes([$attribute => $value]);
175
        }
176
    }
177
178
    /**
179
     * Generate a thumb
180
     *
181
     * @param string $attribute The attribute name
182
     * @param string $preset The preset name
183
     * @param string $path The file path
184
     * @return string The thumb path
185
     */
186
    private function generateThumb($attribute, $preset, $path)
187
    {
188
        $thumbPath = pathinfo($path, PATHINFO_FILENAME);
189
        $thumbPath = str_replace($thumbPath, $preset . '_' . $thumbPath, $path);
190
        $realPath = $this->fileStorage($attribute)->path;
191
192
        if (!file_exists($realPath . $thumbPath) && file_exists($realPath . $path)) {
193
            $handlerPreset = $this->fileOption($attribute, 'preset.'.$preset);
194
            $handlerPreset($realPath, $path, $thumbPath);
195
        }
196
197
        return $thumbPath;
198
    }
199
200
    /**
201
     * Generate file path by template
202
     *
203
     * @param string $attribute The attribute name
204
     * @param ActiveRecord $file The file model
205
     * @return string The file path
206
     */
207
    private function templatePath($attribute, $file = null)
208
    {
209
        $value = $this->owner->{$attribute};
210
211
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
212
        $isFilledPath = $saveFilePath && !empty($value);
213
214
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
215
        $isFilledId = $saveFileId && is_numeric($value) && $value;
216
217
        if (($isFilledPath || $isFilledId) && $file === null) {
218
            $file = $this->file($attribute);
219
        }
220
221
        if ($file !== null) {
222
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
223
            return $handlerTemplatePath($file);
224
        }
225
        return $value;
226
    }
227
228
    /**
229
     * Get relation
230
     *
231
     * @param string $attribute The attribute name
232
     * @return \ActiveQuery
233
     */
234 2
    public function fileRelation($attribute)
235
    {
236 2
        if ($this->relation === null) {
237 2
            $this->relation = $this->owner->getRelation($this->fileOption($attribute, 'relation'));
238
        }
239 2
        return $this->relation;
240
    }
241
242
    /**
243
     * Get file option
244
     *
245
     * @param string $attribute The attribute name
246
     * @param string $option Option name
247
     * @param mixed $defaultValue Default value
248
     * @return mixed
249
     */
250 30
    public function fileOption($attribute, $option, $defaultValue = null)
251
    {
252 30
        return ArrayHelper::getValue($this->attributes[$attribute], $option, $defaultValue);
253
    }
254
255
    /**
256
     * Get file storage
257
     *
258
     * @param string $attribute The attribute name
259
     * @return \Flysystem
260
     */
261 26
    public function fileStorage($attribute)
262
    {
263 26
        return Yii::$app->get($this->fileOption($attribute, 'storage'));
264
    }
265
266
    /**
267
     * Get file path
268
     *
269
     * @param string $attribute The attribute name
270
     * @param ActiveRecord $file Use this file model
271
     * @return string The file path
272
     */
273
    public function filePath($attribute, $file = null)
274
    {
275
        $path = $this->templatePath($attribute, $file);
276
        return $this->fileStorage($attribute)->path . $path;
277
    }
278
279
    /**
280
     * Get file url
281
     *
282
     * @param string $attribute The attribute name
283
     * @param ActiveRecord $file Use this file model
284
     * @return string The file url
285
     */
286
    public function fileUrl($attribute, $file = null)
287
    {
288
        $path = $this->templatePath($attribute, $file);
289
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $path;
290
    }
291
292
    /**
293
     * Get extra fields of file
294
     *
295
     * @param string $attribute The attribute name
296
     * @return array
297
     */
298
    public function fileExtraFields($attribute)
299
    {
300
        $fields = $this->fileBind->relations($this->owner, $attribute);
301
        if (!$this->fileOption($attribute, 'multiple')) {
302
            return array_shift($fields);
303
        }
304
        return $fields;
305
    }
306
307
    /**
308
     * Get files
309
     *
310
     * @param string $attribute The attribute name
311
     * @return \ActiveRecord[] The file models
312
     */
313 1
    public function files($attribute)
314
    {
315 1
        return $this->fileBind->files($this->owner, $attribute);
316
    }
317
318
    /**
319
     * Get the file
320
     *
321
     * @param string $attribute The attribute name
322
     * @return \ActiveRecord The file model
323
     */
324 1
    public function file($attribute)
325
    {
326 1
        return $this->fileBind->file($this->owner, $attribute);
327
    }
328
329
    /**
330
     * Get rules
331
     *
332
     * @param string $attribute The attribute name
333
     * @param bool $onlyCoreValidators Only core validators
334
     * @return array
335
     */
336 29
    public function fileRules($attribute, $onlyCoreValidators = false)
337
    {
338 29
        $rules = $this->fileOption($attribute, 'rules', []);
339 29
        if ($onlyCoreValidators && isset($rules['imageSize'])) {
340 28
            $rules = array_merge($rules, $rules['imageSize']);
341 28
            unset($rules['imageSize']);
342
        }
343 29
        return $rules;
344
    }
345
346
    /**
347
     * Get file state
348
     *
349
     * @param string $attribute The attribute name
350
     * @return array
351
     */
352
    public function fileState($attribute)
353
    {
354
        $query = FileUploadSession::find()->where([
355
            'created_user_id' => Yii::$app->user->id,
356
            'target_model_class' => get_class($this->owner),
357
            'target_model_attribute' => $attribute,
358
        ]);
359
        $query->andWhere(['or',
360
            ['target_model_id' => $this->owner->getPrimaryKey()],
361
            ['target_model_id' => null] // for cases of uploads when original model was a new record at the moment of uploads
362
        ]);
363
364
        $data = $query->all();
365
        if ($data) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $data of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
366
            return ArrayHelper::getColumn($data, ['file_id']);
0 ignored issues
show
Documentation introduced by
array('file_id') is of type array<integer,string,{"0":"string"}>, but the function expects a string|object<Closure>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
367
        } else {
368
            return [];
369
        }
370
    }
371
372
    /**
373
     * Get the presets of the file for apply after upload
374
     *
375
     * @param string $attribute The attribute name
376
     * @return array
377
     */
378
    public function filePresetAfterUpload($attribute)
379
    {
380
        $preset = $this->fileOption($attribute, 'applyPresetAfterUpload', []);
381
        if (is_string($preset) && $preset === '*') {
382
            return array_keys($this->fileOption($attribute, 'preset', []));
383
        }
384
        return $preset;
385
    }
386
387
    /**
388
     * Create a thumb and return url
389
     *
390
     * @param string $attribute The attribute name
391
     * @param string $preset The preset name
392
     * @param ActiveRecord $file Use this file model
393
     * @return string The file url
394
     */
395
    public function thumbUrl($attribute, $preset, $file = null)
396
    {
397
        $path = $this->templatePath($attribute, $file);
398
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
399
400
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $thumbPath;
401
    }
402
403
    /**
404
     * Create a thumb and return full path
405
     *
406
     * @param string $attribute The attribute name
407
     * @param string $preset The preset name
408
     * @param ActiveRecord $file Use this file model
409
     * @return string The file path
410
     */
411
    public function thumbPath($attribute, $preset, $file = null)
412
    {
413
        $path = $this->templatePath($attribute, $file);
414
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
415
416
        return $this->fileStorage($attribute)->path . $thumbPath;
417
    }
418
419
    /**
420
     * Create a file
421
     *
422
     * @param string $attribute The attribute name
423
     * @param string $path The file path
424
     * @param string $name The file name
425
     * @return \ActiveRecord The file model
426
     */
427 25
    public function createFile($attribute, $path, $name)
428
    {
429 25
        $handlerCreateFile = $this->fileOption($attribute, 'createFile');
430 25
        $file = $handlerCreateFile($path, $name);
431 25
        if ($file) {
432 25
            $storage = $this->fileStorage($attribute);
433 25
            $contents = file_get_contents($path);
434 25
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
435 25
            if ($storage->write($handlerTemplatePath($file), $contents)) {
436 25
                $this->setState($attribute, $file);
437
                $this->owner->{$attribute} = $file->id;
438
                return $file;
439
            }
440
        } // @codeCoverageIgnore
441
        return false; // @codeCoverageIgnore
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type documented by rkit\filemanager\behavio...ileBehavior::createFile of type ActiveRecord.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
442
    }
443
}
444