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

FileBehavior::createRemoteFile()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 20

Importance

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