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