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

FileBehavior::addClassAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

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