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

FileBehavior::createRemoteFile()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 22

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 30

Importance

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