Completed
Pull Request — master (#4)
by
unknown
02:11
created

FileBehavior::clearState()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 12.582

Importance

Changes 0
Metric Value
dl 0
loc 23
ccs 3
cts 16
cp 0.1875
rs 9.552
c 0
b 0
f 0
cc 4
nc 5
nop 2
crap 12.582
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
    /**
25
     * @var ActiveQuery
26
     */
27
    private $relation;
28
29
    /**
30
     * @var FileBind
31
     */
32
    private $fileBind;
33
34
    /**
35
     * @var array
36
     */
37
    protected static $classPathMap = [];
38
39
    /**
40
     * @var string name of application component that represents `user`
41
     */
42
    public $userComponent = 'user';
43
44
    /**
45
     * @since 5.6.0
46
     * @var bool
47
     */
48
    protected $markedLinked = false;
49
50
    /**
51
     * @internal
52
     */
53 31
    public function init()
54
    {
55 31
        parent::init();
56
57 31
        $this->fileBind = new FileBind();
58
59 31
        Yii::$app->fileManager->registerTranslations();
60 31
    }
61
62
    /**
63
     * @inheritdoc
64
     * @internal
65
     */
66 31
    public function events()
67
    {
68
        return [
69 31
            ActiveRecord::EVENT_AFTER_INSERT  => 'afterSave',
70 31
            ActiveRecord::EVENT_AFTER_UPDATE  => 'afterSave',
71 31
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
72 31
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
73 31
            ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
74
        ];
75
    }
76
77
    /**
78
     * @internal
79
     * @SuppressWarnings(PHPMD.UnusedFormalParameter)
80
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
81
     */
82 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...
83
    {
84 2
        foreach ($this->attributes as $attribute => $options) {
85 2
            $oldValue = $this->owner->isNewRecord ? null : $this->owner->getOldAttribute($attribute);
86 2
            $isAttributeChanged = $oldValue === null ? true : $this->owner->isAttributeChanged($attribute);
87
88 2
            $this->attributes[$attribute]['isAttributeChanged'] = $isAttributeChanged;
89 2
            $this->attributes[$attribute]['oldValue'] = $oldValue;
90
        }
91 2
    }
92
93
    /**
94
     * @internal
95
     */
96 2
    public function afterSave()
97
    {
98 2
        foreach ($this->attributes as $attribute => $options) {
99 2
            $files = $this->owner->{$attribute};
100
101 2
            $isAttributeNotChanged = $options['isAttributeChanged'] === false || $files === null;
102 2
            if ($isAttributeNotChanged) {
103 2
                continue;
104
            }
105
106 2
            if (is_numeric($files)) {
107
                $files = [$files];
108
            }
109
110 2
            if (is_array($files)) {
111 1
                $files = array_filter($files);
112
            }
113
114 2
            if ($files === [] || $files === '') {
115 1
                $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
116 1
                continue;
117
            }
118
119 1
            $maxFiles = ArrayHelper::getValue($this->fileRules($attribute, true), 'maxFiles');
120 1
            if (is_array($files) && $maxFiles !== null) {
121 1
                $files = array_slice($files, 0, $maxFiles, true);
122
            }
123
124 1
            $files = $this->fileBind->bind($this->owner, $attribute, $files);
125
126 1
            $this->clearState($attribute, $files);
127
128 1
            if (is_array($files)) {
129
                $files = array_shift($files);
130 1
                $this->setValue($attribute, $files, $options['oldValue']);
131
            }
132
        }
133 2
    }
134
135
    /**
136
     * @internal
137
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
138
     */
139
    public function beforeDelete()
140
    {
141
        foreach ($this->attributes as $attribute => $options) {
142
            $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
143
        }
144
    }
145
146 26
    protected function getUser()
147
    {
148 26
        if (!$this->userComponent || !isset(Yii::$app->{$this->userComponent})) {
149 26
            return false;
150
        }
151
        return Yii::$app->{$this->userComponent};
152
    }
153
154 1
    public function clearState($attribute, $files)
155
    {
156 1
        if (!$this->getUser()) {
157 1
            return [];
158
        }
159
        if (!is_array($files)) {
160
            $files = [$files];
161
        }
162
        $query = [
163
            'created_user_id' => $this->getUser()->id,
164
            'target_model_class' => static::getClass(get_class($this->owner)),
165
            'target_model_id' => $this->owner->getPrimaryKey(),
166
            'target_model_attribute' => $attribute,
167
        ];
168
        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...
169
            $fileIDs = ArrayHelper::getColumn($files, 'id');
170
            $query['file_id'] = $fileIDs;
171
        }
172
        FileUploadSession::deleteAll($query);
173
        $query['target_model_id'] = null;
174
        FileUploadSession::deleteAll($query);  // for cases of uploads when original model was a new record at the moment of uploads
175
        return;
176
    }
177
178 25
    private function setState($attribute, $file)
179
    {
180 25
        $rec = new FileUploadSession();
181 25
        $rec->created_user_id = $this->getUser()->id;
182
        $rec->file_id = $file->getPrimaryKey();
183
        $rec->target_model_attribute = $attribute; // TODO: write model/object id?
184
        $rec->target_model_id = (!$this->owner->isNewRecord ? $this->owner->getPrimaryKey() : null);
185
        $rec->target_model_class = static::getClass(get_class($this->owner));
186
        $rec->save(false);
187
    }
188
189
    /**
190
     * for models with single upload only
191
     * @param $attribute
192
     * @param $file
193
     * @param $defaultValue
194
     */
195
    private function setValue($attribute, $file, $defaultValue)
196
    {
197
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
198
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
199
200
        if ($saveFilePath || $saveFileId) {
201
            if (!$file) {
202
                $value = $defaultValue;
203
            } elseif ($saveFilePath) {
204
                $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
205
                $value = Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $handlerTemplatePath($file);
206
            } elseif ($saveFileId) {
207
                $value = $file->getPrimaryKey();
208
            }
209
            $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...
210
            $this->owner->updateAttributes([$attribute => $value]);
211
        }
212
    }
213
214
    /**
215
     * Generate a thumb
216
     *
217
     * @param string $attribute The attribute name
218
     * @param string $preset The preset name
219
     * @param string $path The file path
220
     * @return string The thumb path
221
     */
222
    private function generateThumb($attribute, $preset, $path)
223
    {
224
        $thumbPath = pathinfo($path, PATHINFO_FILENAME);
225
        $thumbPath = str_replace($thumbPath, $preset . '_' . $thumbPath, $path);
226
        $realPath = $this->fileStorage($attribute)->path;
227
228
        if (!file_exists($realPath . $thumbPath) && file_exists($realPath . $path)) {
229
            $handlerPreset = $this->fileOption($attribute, 'preset.'.$preset);
230
            $handlerPreset($realPath, $path, $thumbPath);
231
        }
232
233
        return $thumbPath;
234
    }
235
236
    /**
237
     * Generate file path by template
238
     *
239
     * @param string $attribute The attribute name
240
     * @param ActiveRecord $file The file model
241
     * @return string The file path
242
     */
243
    private function templatePath($attribute, $file = null)
244
    {
245
        $value = $this->owner->{$attribute};
246
247
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
248
        $isFilledPath = $saveFilePath && !empty($value);
249
250
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
251
        $isFilledId = $saveFileId && is_numeric($value) && $value;
252
253
        if (($isFilledPath || $isFilledId) && $file === null) {
254
            $file = $this->file($attribute);
255
        }
256
257
        if ($file !== null) {
258
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
259
            return $handlerTemplatePath($file);
260
        }
261
        return $value;
262
    }
263
264
    /**
265
     * Get relation
266
     *
267
     * @param string $attribute The attribute name
268
     * @return \ActiveQuery
269
     */
270 2
    public function fileRelation($attribute)
271
    {
272 2
        if ($this->relation === null) {
273 2
            $this->relation = $this->owner->getRelation($this->fileOption($attribute, 'relation'));
274
        }
275 2
        return $this->relation;
276
    }
277
278
    /**
279
     * Get file option
280
     *
281
     * @param string $attribute The attribute name
282
     * @param string $option Option name
283
     * @param mixed $defaultValue Default value
284
     * @return mixed
285
     */
286 30
    public function fileOption($attribute, $option, $defaultValue = null)
287
    {
288 30
        return ArrayHelper::getValue($this->attributes[$attribute], $option, $defaultValue);
289
    }
290
291
    /**
292
     * Get file storage
293
     *
294
     * @param string $attribute The attribute name
295
     * @return \Flysystem
296
     */
297 26
    public function fileStorage($attribute)
298
    {
299 26
        return Yii::$app->get($this->fileOption($attribute, 'storage'));
300
    }
301
302
    /**
303
     * Get file path
304
     *
305
     * @param string $attribute The attribute name
306
     * @param ActiveRecord $file Use this file model
307
     * @return string The file path
308
     */
309
    public function filePath($attribute, $file = null)
310
    {
311
        $path = $this->templatePath($attribute, $file);
312
        return $this->fileStorage($attribute)->path . $path;
313
    }
314
315
    /**
316
     * Get file url
317
     *
318
     * @param string $attribute The attribute name
319
     * @param ActiveRecord $file Use this file model
320
     * @return string The file url
321
     */
322
    public function fileUrl($attribute, $file = null)
323
    {
324
        $path = $this->templatePath($attribute, $file);
325
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $path;
326
    }
327
328
    /**
329
     * Get extra fields of file
330
     *
331
     * @param string $attribute The attribute name
332
     * @return array
333
     */
334
    public function fileExtraFields($attribute)
335
    {
336
        $fields = $this->fileBind->relations($this->owner, $attribute);
337
        if (!$this->fileOption($attribute, 'multiple')) {
338
            return array_shift($fields);
339
        }
340
        return $fields;
341
    }
342
343
    /**
344
     * Get files
345
     *
346
     * @param string $attribute The attribute name
347
     * @return \ActiveRecord[] The file models
348
     */
349 2
    public function files($attribute)
350
    {
351 2
        return $this->fileBind->files($this->owner, $attribute);
352
    }
353
354
    /**
355
     * Get the file
356
     *
357
     * @param string $attribute The attribute name
358
     * @return \ActiveRecord The file model
359
     */
360 1
    public function file($attribute)
361
    {
362 1
        return $this->fileBind->file($this->owner, $attribute);
363
    }
364
365
    /**
366
     * Get rules
367
     *
368
     * @param string $attribute The attribute name
369
     * @param bool $onlyCoreValidators Only core validators
370
     * @return array
371
     */
372 29
    public function fileRules($attribute, $onlyCoreValidators = false)
373
    {
374 29
        $rules = $this->fileOption($attribute, 'rules', []);
375 29
        if ($onlyCoreValidators && isset($rules['imageSize'])) {
376 28
            $rules = array_merge($rules, $rules['imageSize']);
377 28
            unset($rules['imageSize']);
378
        }
379 29
        return $rules;
380
    }
381
382
    /**
383
     * Get file state
384
     *
385
     * @param string $attribute The attribute name
386
     * @return array
387
     */
388
    public function fileState($attribute)
389
    {
390
        if (!$this->getUser()) {
391
            return [];
392
        }
393
        $query = FileUploadSession::find()->where([
394
            'created_user_id' => $this->getUser()->id,
395
            'target_model_class' => static::getClass(get_class($this->owner)),
396
            'target_model_attribute' => $attribute,
397
        ]);
398
        $query->andWhere(['or',
399
            ['target_model_id' => $this->owner->getPrimaryKey()],
400
            ['target_model_id' => null] // for cases of uploads when original model was a new record at the moment of uploads
401
        ]);
402
        $data = $query->all();
403
        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...
404
            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...
405
        } else {
406
            return [];
407
        }
408
    }
409
410
    /**
411
     * Get the presets of the file for apply after upload
412
     *
413
     * @param string $attribute The attribute name
414
     * @return array
415
     */
416
    public function filePresetAfterUpload($attribute)
417
    {
418
        $preset = $this->fileOption($attribute, 'applyPresetAfterUpload', []);
419
        if (is_string($preset) && $preset === '*') {
420
            return array_keys($this->fileOption($attribute, 'preset', []));
421
        }
422
        return $preset;
423
    }
424
425
    /**
426
     * Create a thumb and return url
427
     *
428
     * @param string $attribute The attribute name
429
     * @param string $preset The preset name
430
     * @param ActiveRecord $file Use this file model
431
     * @return string The file url
432
     */
433
    public function thumbUrl($attribute, $preset, $file = null)
434
    {
435
        $path = $this->templatePath($attribute, $file);
436
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
437
438
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $thumbPath;
439
    }
440
441
    /**
442
     * Create a thumb and return full path
443
     *
444
     * @param string $attribute The attribute name
445
     * @param string $preset The preset name
446
     * @param ActiveRecord $file Use this file model
447
     * @return string The file path
448
     */
449
    public function thumbPath($attribute, $preset, $file = null)
450
    {
451
        $path = $this->templatePath($attribute, $file);
452
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
453
454
        return $this->fileStorage($attribute)->path . $thumbPath;
455
    }
456
457
    /**
458
     * Create a file
459
     *
460
     * @param string $attribute The attribute name
461
     * @param string $path The file path
462
     * @param string $name The file name
463
     * @return \ActiveRecord The file model
464
     */
465 25
    public function createFile($attribute, $path, $name)
466
    {
467 25
        $handlerCreateFile = $this->fileOption($attribute, 'createFile');
468 25
        $file = $handlerCreateFile($path, $name);
469 25
        if ($file) {
470 25
            $storage = $this->fileStorage($attribute);
471 25
            $contents = file_get_contents($path);
472 25
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
473 25
            if ($storage->write($handlerTemplatePath($file), $contents)) {
474 25
                if (!$this->markedLinked) {
475 25
                    $this->setState($attribute, $file);
476
                }
477
                $this->owner->{$attribute} = $file->id;
478
                return $file;
479
            }
480
        } // @codeCoverageIgnore
481
        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...
482
    }
483
484
    /**
485
     * Create a file from remote URL
486
     *
487
     * @author Sergii Gamaiunov <[email protected]>
488
     *
489
     * @param string $attribute The attribute name
490
     * @param \igogo5yo\uploadfromurl\UploadFromUrl $remoteFile
491
     * @param string $name The file name
492
     * @return \ActiveRecord The file model
493
     */
494
    public function createRemoteFile($attribute, $remoteFile, $name)
495
    {
496
        $url = $remoteFile->url;
497
        $handlerCreateFile = $this->fileOption($attribute, 'createRemoteFile');
498
        $file = $handlerCreateFile($remoteFile, $name);
499
        if ($file) {
500
            $storage = $this->fileStorage($attribute);
501
            $stream = fopen($url, 'r');
502
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
503
            if ($storage->putStream($handlerTemplatePath($file), $stream)) {
504
                if (is_resource($stream)) { // some adapters close resources on their own
505
                    fclose($stream);
506
                }
507
                if ($this->getUser()) {
508
                    if (!$this->markedLinked) {
509
                        $this->setState($attribute, $file);
510
                    }
511
                }
512
                $this->owner->{$attribute} = $file->id;
513
                return $file;
514
            }
515
        } // @codeCoverageIgnore
516
        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...avior::createRemoteFile 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...
517
    }
518
519
    /**
520
     * Add class alias to be able to upload files for different versions of a model to a single API endpoint
521
     *
522
     * Example:
523
     * ```
524
     * class OldCar extends Car
525
     * {
526
     *      public function init()
527
     *      {
528
     *          parent::init();
529
     *          $this->car_type = 'old;
530
     *          FileBehavior::addClassAlias(get_class($this), Car::className());
531
     *      }
532
     *
533
     *      public function formName() {
534
     *          return 'Car';
535
     *      }
536
     * }
537
     * ```
538
     * @param $source
539
     * @param $mapTo
540
     */
541
    public static function addClassAlias($source, $mapTo) {
542
        static::$classPathMap[$source] = $mapTo;
543
    }
544
545
    protected static function getClass($source) {
546
        return isset(static::$classPathMap[$source])
547
            ? static::$classPathMap[$source]
548
            : $source;
549
    }
550
551
    /**
552
     * Mark current upload session as already linked (e.g. file is linked during `createFile`) to avoid duplicate links
553
     * @return $this
554
     * @since 5.6.0
555
     */
556
    public function markLinked() {
557
        $this->markedLinked = true;
558
        return $this;
559
    }
560
}
561