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

FileBehavior::thumbUrl()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 7
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 3
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
     * @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
        ];
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
        }
73 2
    }
74
75
    /**
76
     * @internal
77
     */
78 2
    public function afterSave()
79
    {
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
            }
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
            }
105
106 1
            $files = $this->fileBind->bind($this->owner, $attribute, $files);
107 1
            if (is_array($files)) {
108
                $files = array_shift($files);
109
            }
110
111 1
            $this->clearState($attribute);
112
            $this->setValue($attribute, $files, $options['oldValue']);
113
        }
114 1
    }
115
116
    /**
117
     * @internal
118
     * @SuppressWarnings(PHPMD.UnusedLocalVariable)
119
     */
120
    public function beforeDelete()
121
    {
122
        foreach ($this->attributes as $attribute => $options) {
123
            $this->fileBind->delete($this->owner, $attribute, $this->files($attribute));
124
        }
125
    }
126
127 1
    private function clearState($attribute)
128
    {
129 1
        FileUploadSession::deleteAll([
130 1
            'created_user_id' => Yii::$app->user->id,
131 1
            'target_model_class' => get_class($this->owner),
132 1
            'target_model_id' => $this->owner->getPrimaryKey(),
133 1
            'target_model_attribute' => $attribute,
134
        ]);
135
    }
136
137 25
    private function setState($attribute, $file)
138
    {
139 25
        $rec = new FileUploadSession();
140 25
        $rec->created_user_id = Yii::$app->user->id;
141
        $rec->file_id = $file->getPrimaryKey();
142
        $rec->target_model_attribute = $attribute; // TODO: write model/object id?
143
        $rec->target_model_id = (!$this->owner->isNewRecord ? $this->owner->getPrimaryKey() : null);
144
        $rec->target_model_class = get_class($this->owner);
145
        $rec->save(false);
146
    }
147
148
    private function setValue($attribute, $file, $defaultValue)
149
    {
150
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
151
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
152
153
        if ($saveFilePath || $saveFileId) {
154
            if (!$file) {
155
                $value = $defaultValue;
156
            } elseif ($saveFilePath) {
157
                $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
158
                $value = Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $handlerTemplatePath($file);
159
            } elseif ($saveFileId) {
160
                $value = $file->getPrimaryKey();
161
            }
162
            $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...
163
            $this->owner->updateAttributes([$attribute => $value]);
164
        }
165
    }
166
167
    /**
168
     * Generate a thumb
169
     *
170
     * @param string $attribute The attribute name
171
     * @param string $preset The preset name
172
     * @param string $path The file path
173
     * @return string The thumb path
174
     */
175
    private function generateThumb($attribute, $preset, $path)
176
    {
177
        $thumbPath = pathinfo($path, PATHINFO_FILENAME);
178
        $thumbPath = str_replace($thumbPath, $preset . '_' . $thumbPath, $path);
179
        $realPath = $this->fileStorage($attribute)->path;
180
181
        if (!file_exists($realPath . $thumbPath) && file_exists($realPath . $path)) {
182
            $handlerPreset = $this->fileOption($attribute, 'preset.'.$preset);
183
            $handlerPreset($realPath, $path, $thumbPath);
184
        }
185
186
        return $thumbPath;
187
    }
188
189
    /**
190
     * Generate file path by template
191
     *
192
     * @param string $attribute The attribute name
193
     * @param ActiveRecord $file The file model
194
     * @return string The file path
195
     */
196
    private function templatePath($attribute, $file = null)
197
    {
198
        $value = $this->owner->{$attribute};
199
200
        $saveFilePath = $this->fileOption($attribute, 'saveFilePathInAttribute');
201
        $isFilledPath = $saveFilePath && !empty($value);
202
203
        $saveFileId = $this->fileOption($attribute, 'saveFileIdInAttribute');
204
        $isFilledId = $saveFileId && is_numeric($value) && $value;
205
206
        if (($isFilledPath || $isFilledId) && $file === null) {
207
            $file = $this->file($attribute);
208
        }
209
210
        if ($file !== null) {
211
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
212
            return $handlerTemplatePath($file);
213
        }
214
        return $value;
215
    }
216
217
    /**
218
     * Get relation
219
     *
220
     * @param string $attribute The attribute name
221
     * @return \ActiveQuery
222
     */
223 2
    public function fileRelation($attribute)
224
    {
225 2
        if ($this->relation === null) {
226 2
            $this->relation = $this->owner->getRelation($this->fileOption($attribute, 'relation'));
227
        }
228 2
        return $this->relation;
229
    }
230
231
    /**
232
     * Get file option
233
     *
234
     * @param string $attribute The attribute name
235
     * @param string $option Option name
236
     * @param mixed $defaultValue Default value
237
     * @return mixed
238
     */
239 30
    public function fileOption($attribute, $option, $defaultValue = null)
240
    {
241 30
        return ArrayHelper::getValue($this->attributes[$attribute], $option, $defaultValue);
242
    }
243
244
    /**
245
     * Get file storage
246
     *
247
     * @param string $attribute The attribute name
248
     * @return \Flysystem
249
     */
250 26
    public function fileStorage($attribute)
251
    {
252 26
        return Yii::$app->get($this->fileOption($attribute, 'storage'));
253
    }
254
255
    /**
256
     * Get file path
257
     *
258
     * @param string $attribute The attribute name
259
     * @param ActiveRecord $file Use this file model
260
     * @return string The file path
261
     */
262
    public function filePath($attribute, $file = null)
263
    {
264
        $path = $this->templatePath($attribute, $file);
265
        return $this->fileStorage($attribute)->path . $path;
266
    }
267
268
    /**
269
     * Get file url
270
     *
271
     * @param string $attribute The attribute name
272
     * @param ActiveRecord $file Use this file model
273
     * @return string The file url
274
     */
275
    public function fileUrl($attribute, $file = null)
276
    {
277
        $path = $this->templatePath($attribute, $file);
278
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $path;
279
    }
280
281
    /**
282
     * Get extra fields of file
283
     *
284
     * @param string $attribute The attribute name
285
     * @return array
286
     */
287
    public function fileExtraFields($attribute)
288
    {
289
        $fields = $this->fileBind->relations($this->owner, $attribute);
290
        if (!$this->fileOption($attribute, 'multiple')) {
291
            return array_shift($fields);
292
        }
293
        return $fields;
294
    }
295
296
    /**
297
     * Get files
298
     *
299
     * @param string $attribute The attribute name
300
     * @return \ActiveRecord[] The file models
301
     */
302 1
    public function files($attribute)
303
    {
304 1
        return $this->fileBind->files($this->owner, $attribute);
305
    }
306
307
    /**
308
     * Get the file
309
     *
310
     * @param string $attribute The attribute name
311
     * @return \ActiveRecord The file model
312
     */
313 1
    public function file($attribute)
314
    {
315 1
        return $this->fileBind->file($this->owner, $attribute);
316
    }
317
318
    /**
319
     * Get rules
320
     *
321
     * @param string $attribute The attribute name
322
     * @param bool $onlyCoreValidators Only core validators
323
     * @return array
324
     */
325 29
    public function fileRules($attribute, $onlyCoreValidators = false)
326
    {
327 29
        $rules = $this->fileOption($attribute, 'rules', []);
328 29
        if ($onlyCoreValidators && isset($rules['imageSize'])) {
329 28
            $rules = array_merge($rules, $rules['imageSize']);
330 28
            unset($rules['imageSize']);
331
        }
332 29
        return $rules;
333
    }
334
335
    /**
336
     * Get file state
337
     *
338
     * @param string $attribute The attribute name
339
     * @return array
340
     */
341
    public function fileState($attribute)
342
    {
343
        $data = FileUploadSession::find()->where([
344
            'created_user_id' => Yii::$app->user->id,
345
            'target_model_class' => get_class($this->owner),
346
            'target_model_id' => $this->owner->getPrimaryKey(),
347
            'target_model_attribute' => $attribute,
348
            ])->all();
349
        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...
350
            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...
351
        } else {
352
            return [];
353
        }
354
    }
355
356
    /**
357
     * Get the presets of the file for apply after upload
358
     *
359
     * @param string $attribute The attribute name
360
     * @return array
361
     */
362
    public function filePresetAfterUpload($attribute)
363
    {
364
        $preset = $this->fileOption($attribute, 'applyPresetAfterUpload', []);
365
        if (is_string($preset) && $preset === '*') {
366
            return array_keys($this->fileOption($attribute, 'preset', []));
367
        }
368
        return $preset;
369
    }
370
371
    /**
372
     * Create a thumb and return url
373
     *
374
     * @param string $attribute The attribute name
375
     * @param string $preset The preset name
376
     * @param ActiveRecord $file Use this file model
377
     * @return string The file url
378
     */
379
    public function thumbUrl($attribute, $preset, $file = null)
380
    {
381
        $path = $this->templatePath($attribute, $file);
382
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
383
384
        return Yii::getAlias($this->fileOption($attribute, 'baseUrl')) . $thumbPath;
385
    }
386
387
    /**
388
     * Create a thumb and return full path
389
     *
390
     * @param string $attribute The attribute name
391
     * @param string $preset The preset name
392
     * @param ActiveRecord $file Use this file model
393
     * @return string The file path
394
     */
395
    public function thumbPath($attribute, $preset, $file = null)
396
    {
397
        $path = $this->templatePath($attribute, $file);
398
        $thumbPath = $this->generateThumb($attribute, $preset, $path);
399
400
        return $this->fileStorage($attribute)->path . $thumbPath;
401
    }
402
403
    /**
404
     * Create a file
405
     *
406
     * @param string $attribute The attribute name
407
     * @param string $path The file path
408
     * @param string $name The file name
409
     * @return \ActiveRecord The file model
410
     */
411 25
    public function createFile($attribute, $path, $name)
412
    {
413 25
        $handlerCreateFile = $this->fileOption($attribute, 'createFile');
414 25
        $file = $handlerCreateFile($path, $name);
415 25
        if ($file) {
416 25
            $storage = $this->fileStorage($attribute);
417 25
            $contents = file_get_contents($path);
418 25
            $handlerTemplatePath = $this->fileOption($attribute, 'templatePath');
419 25
            if ($storage->write($handlerTemplatePath($file), $contents)) {
420 25
                $this->setState($attribute, $file);
421
                $this->owner->{$attribute} = $file->id;
422
                return $file;
423
            }
424
        } // @codeCoverageIgnore
425
        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...
426
    }
427
}
428