UploadImageBehavior::getThumbUploadPath()   A
last analyzed

Complexity

Conditions 3
Paths 4

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 10
ccs 6
cts 6
cp 1
rs 9.9332
c 0
b 0
f 0
cc 3
nc 4
nop 3
crap 3
1
<?php
2
3
namespace mohorev\file;
4
5
use Imagine\Image\ManipulatorInterface;
6
use Yii;
7
use yii\base\InvalidArgumentException;
8
use yii\base\InvalidConfigException;
9
use yii\base\NotSupportedException;
10
use yii\db\BaseActiveRecord;
11
use yii\helpers\ArrayHelper;
12
use yii\helpers\FileHelper;
13
use yii\imagine\Image;
14
15
/**
16
 * UploadImageBehavior automatically uploads image, creates thumbnails and fills
17
 * the specified attribute with a value of the name of the uploaded image.
18
 *
19
 * To use UploadImageBehavior, insert the following code to your ActiveRecord class:
20
 *
21
 * ```php
22
 * use mohorev\file\UploadImageBehavior;
23
 *
24
 * function behaviors()
25
 * {
26
 *     return [
27
 *         [
28
 *             'class' => UploadImageBehavior::class,
29
 *             'attribute' => 'file',
30
 *             'scenarios' => ['insert', 'update'],
31
 *             'placeholder' => '@app/modules/user/assets/images/userpic.jpg',
32
 *             'path' => '@webroot/upload/{id}/images',
33
 *             'url' => '@web/upload/{id}/images',
34
 *             'thumbPath' => '@webroot/upload/{id}/images/thumb',
35
 *             'thumbUrl' => '@web/upload/{id}/images/thumb',
36
 *             'createThumbsOnSave' => false,
37
 *             'createThumbsOnRequest' => true,
38
 *             'thumbs' => [
39
 *                   'thumb' => ['width' => 400, 'quality' => 90],
40
 *                   'preview' => ['width' => 200, 'height' => 200],
41
 *              ],
42
 *         ],
43
 *     ];
44
 * }
45
 * ```
46
 *
47
 * @author Alexander Mohorev <[email protected]>
48
 * @author Alexey Samoylov <[email protected]>
49
 */
50
class UploadImageBehavior extends UploadBehavior
51
{
52
    /**
53
     * @var string
54
     */
55
    public $placeholder;
56
    /**
57
     * create all thumbs profiles on image upload
58
     * @var boolean
59
     */
60
    public $createThumbsOnSave = true;
61
    /**
62
     * create thumb only for profile request by getThumbUploadUrl() method
63
     * @var boolean
64
     */
65
    public $createThumbsOnRequest = false;
66
    /**
67
     * Whether delete original uploaded image after thumbs generating.
68
     * Defaults to FALSE
69
     * @var boolean
70
     */
71
    public $deleteOriginalFile = false;
72
    /**
73
     * @var array the thumbnail profiles
74
     * - `width`
75
     * - `height`
76
     * - `quality`
77
     */
78
    public $thumbs = [
79
        'thumb' => ['width' => 200, 'height' => 200, 'quality' => 90],
80
    ];
81
    /**
82
     * @var string|null
83
     */
84
    public $thumbPath;
85
    /**
86
     * @var string|null
87
     */
88
    public $thumbUrl;
89
90
91
    /**
92
     * @inheritdoc
93
     */
94 8
    public function init()
95
    {
96 8
        if (!class_exists(Image::class)) {
97
            throw new NotSupportedException("Yii2-imagine extension is required to use the UploadImageBehavior");
98
        }
99
100 8
        parent::init();
101
102 8
        if ($this->thumbPath === null) {
103 8
            $this->thumbPath = $this->path;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->path of type callable is incompatible with the declared type string|null of property $thumbPath.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
104
        }
105 8
        if ($this->thumbUrl === null) {
106 8
            $this->thumbUrl = $this->url;
0 ignored issues
show
Documentation Bug introduced by
It seems like $this->url of type callable is incompatible with the declared type string|null of property $thumbUrl.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
107
        }
108
109 8
        foreach ($this->thumbs as $config) {
110 8
            $width = ArrayHelper::getValue($config, 'width');
111 8
            $height = ArrayHelper::getValue($config, 'height');
112 8
            if ($height < 1 && $width < 1) {
113
                throw new InvalidConfigException(sprintf(
114
                    'Length of either side of thumb cannot be 0 or negative, current size ' .
115
                    'is %sx%s', $width, $height
116
                ));
117
            }
118
        }
119 8
    }
120
121
    /**
122
     * @param string $attribute
123
     * @param string $profile
124
     * @return string|null
125
     * @throws \yii\base\Exception
126
     * @throws \yii\base\InvalidConfigException
127
     */
128 5
    public function getThumbUploadUrl($attribute, $profile = 'thumb')
129
    {
130
        /** @var BaseActiveRecord $model */
131 5
        $model = $this->owner;
132
133 5
        if ($this->attribute !== $attribute) {
134 3
            $behaviors = $model->getBehaviors();
135
136 3
            foreach ($behaviors as $behavior) {
0 ignored issues
show
Bug introduced by
The expression $behaviors of type array<integer,object<yii\base\Behavior>>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
137 3
                if ($behavior instanceof UploadImageBehavior) {
138 3
                    if ($behavior->attribute == $attribute) {
139 2
                        return $behavior->getThumbUploadUrl($attribute, $profile);
140
                    }
141
                }
142
            }
143
        }
144
145 5
        if (!$model->getAttribute($attribute)) {
146 3
            if ($this->placeholder) {
147 3
                return $this->getPlaceholderUrl($profile);
148
            } else {
149
                return null;
150
            }
151
        }
152
153 4
        $path = $this->getUploadPath($attribute, true);
154
155
        //if original file exist - generate profile thumb and generate url to thumb
156 4
        if (is_file($path) || !$this->deleteOriginalFile) {
157 4
            if ($this->createThumbsOnRequest) {
158 4
                $this->createThumbs($profile);
159
            }
160 4
            return $this->getThumbProfileUrl($attribute, $profile, $model);
161
        } //if original file is deleted generate url to thumb
162
        elseif ($this->deleteOriginalFile) {
163
            return $this->getThumbProfileUrl($attribute, $profile, $model);
164
        } elseif ($this->placeholder) {
165
            return $this->getPlaceholderUrl($profile);
166
        } else {
167
            return null;
168
        }
169
    }
170
171
    /**
172
     * @param $profile
173
     * @return string
174
     */
175 3
    protected function getPlaceholderUrl($profile)
176
    {
177 3
        list ($path, $url) = Yii::$app->assetManager->publish($this->placeholder);
178 3
        $filename = basename($path);
179 3
        $thumb = $this->getThumbFileName($filename, $profile);
180 3
        $thumbPath = dirname($path) . DIRECTORY_SEPARATOR . $thumb;
181 3
        $thumbUrl = dirname($url) . '/' . $thumb;
182
183 3
        if (!is_file($thumbPath)) {
184 1
            $this->generateImageThumb($this->thumbs[$profile], $path, $thumbPath);
185
        }
186
187 3
        return $thumbUrl;
188
    }
189
190
    /**
191
     * @param $attribute
192
     * @param $profile
193
     * @param BaseActiveRecord $model
194
     * @return bool|string
195
     */
196 4
    protected function getThumbProfileUrl($attribute, $profile, BaseActiveRecord $model)
197
    {
198 4
        $url = $this->resolvePath($this->thumbUrl);
199 4
        $fileName = $model->getOldAttribute($attribute);
200 4
        $thumbName = $this->getThumbFileName($fileName, $profile);
201
202 4
        return Yii::getAlias($url . '/' . $thumbName);
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208 5
    protected function afterUpload()
209
    {
210 5
        parent::afterUpload();
211 5
        if ($this->createThumbsOnSave) {
212
            $this->createThumbs();
213
        }
214 5
    }
215
216
    /**
217
     * @param string $needed_profile - profile name to create thumb
218
     * @throws \yii\base\Exception
219
     * @throws \yii\base\InvalidConfigException
220
     */
221 4
    protected function createThumbs($needed_profile = false)
222
    {
223 4
        $path = $this->getUploadPath($this->attribute);
224 4
        foreach ($this->thumbs as $profile => $config) {
225
            //skip profiles not needed now
226 4
            if ($needed_profile && $needed_profile != $profile) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $needed_profile of type false|string is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
227 4
                continue;
228
            }
229
230 4
            $thumbPath = $this->getThumbUploadPath($this->attribute, $profile);
231 4
            if ($thumbPath !== null) {
232 4
                if (!FileHelper::createDirectory(dirname($thumbPath))) {
233
                    throw new InvalidArgumentException(
234
                        "Directory specified in 'thumbPath' attribute doesn't exist or cannot be created."
235
                    );
236
                }
237 4
                if (!is_file($thumbPath) && !file_exists($thumbPath)) {
238 4
                    $this->generateImageThumb($config, $path, $thumbPath);
239
                }
240
            }
241
        }
242
243 4
        if ($this->deleteOriginalFile) {
244
            $this->deleteFile($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->getUploadPath($this->attribute) on line 223 can also be of type boolean or null; however, mohorev\file\UploadBehavior::deleteFile() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
245
        }
246 4
    }
247
248
    /**
249
     * @param string $attribute
250
     * @param string $profile
251
     * @param boolean $old
252
     * @return string
253
     * @throws \yii\base\InvalidConfigException
254
     */
255 4
    public function getThumbUploadPath($attribute, $profile = 'thumb', $old = false)
256
    {
257
        /** @var BaseActiveRecord $model */
258 4
        $model = $this->owner;
259 4
        $path = $this->resolvePath($this->thumbPath);
260 4
        $attribute = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
261 4
        $filename = $this->getThumbFileName($attribute, $profile);
262
263 4
        return $filename ? Yii::getAlias($path . '/' . $filename) : null;
264
    }
265
266
    /**
267
     * @param $filename
268
     * @param string $profile
269
     * @return string
270
     */
271 5
    protected function getThumbFileName($filename, $profile = 'thumb')
272
    {
273 5
        return $profile . '-' . $filename;
274
    }
275
276
    /**
277
     * @param $config
278
     * @param $path
279
     * @param $thumbPath
280
     */
281 5
    protected function generateImageThumb($config, $path, $thumbPath)
282
    {
283 5
        $width = ArrayHelper::getValue($config, 'width');
284 5
        $height = ArrayHelper::getValue($config, 'height');
285 5
        $quality = ArrayHelper::getValue($config, 'quality', 100);
286 5
        $mode = ArrayHelper::getValue($config, 'mode', ManipulatorInterface::THUMBNAIL_INSET);
287 5
        $bg_color = ArrayHelper::getValue($config, 'bg_color', 'FFF');
288
289 5
        if (!$width || !$height) {
290 4
            $image = Image::getImagine()->open($path);
291 4
            $ratio = $image->getSize()->getWidth() / $image->getSize()->getHeight();
292 4
            if ($width) {
293 4
                $height = ceil($width / $ratio);
294
            } else {
295
                $width = ceil($height * $ratio);
296
            }
297
        }
298
299
        // Fix error "PHP GD Allowed memory size exhausted".
300 5
        ini_set('memory_limit', '512M');
301
        //for big images size
302 5
        ini_set('max_execution_time', 60);
303 5
        Image::$thumbnailBackgroundColor = $bg_color;
304 5
        Image::thumbnail($path, $width, $height, $mode)->save($thumbPath, ['quality' => $quality]);
305 5
    }
306
307
    /**
308
     * @inheritdoc
309
     */
310 3
    protected function delete($attribute, $old = false)
311
    {
312 3
        $profiles = array_keys($this->thumbs);
313 3
        foreach ($profiles as $profile) {
314 3
            $path = $this->getThumbUploadPath($attribute, $profile, $old);
315 3
            $this->deleteFile($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->getThumbUploadPat...ribute, $profile, $old) on line 314 can also be of type boolean or null; however, mohorev\file\UploadBehavior::deleteFile() does only seem to accept string, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
316
        }
317 3
        parent::delete($attribute, $old);
318 3
    }
319
}
320