Completed
Push — master ( df60ac...e96430 )
by Alex
26:03 queued 24:52
created

UploadImageBehavior::createThumbs()   B

Complexity

Conditions 9
Paths 11

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 9.648

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 12
cts 15
cp 0.8
rs 8.0555
c 0
b 0
f 0
cc 9
nc 11
nop 1
crap 9.648
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 5
    public function init()
95
    {
96 5
        if (!class_exists(Image::class)) {
97
            throw new NotSupportedException("Yii2-imagine extension is required to use the UploadImageBehavior");
98
        }
99
100 5
        parent::init();
101
102 5
        if ($this->thumbPath === null) {
103 5
            $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 5
        if ($this->thumbUrl === null) {
106 5
            $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 5
        foreach ($this->thumbs as $config) {
110 5
            $width = ArrayHelper::getValue($config, 'width');
111 5
            $height = ArrayHelper::getValue($config, 'height');
112 5
            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 5
    }
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 3
    public function getThumbUploadUrl($attribute, $profile = 'thumb')
129
    {
130
        /** @var BaseActiveRecord $model */
131 3
        $model = $this->owner;
132
133 3
        if (!$model->getAttribute($attribute)) {
134 1
            if ($this->placeholder) {
135 1
                return $this->getPlaceholderUrl($profile);
136
            } else {
137
                return null;
138
            }
139
        }
140
141 2
        $path = $this->getUploadPath($attribute, true);
142
143
        //if original file exist - generate profile thumb and generate url to thumb
144 2
        if (is_file($path) || !$this->deleteOriginalFile) {
145 2
            if ($this->createThumbsOnRequest) {
146 2
                $this->createThumbs($profile);
147
            }
148 2
            return $this->getThumbProfileUrl($attribute, $profile, $model);
149
        } //if original file is deleted generate url to thumb
150
        elseif ($this->deleteOriginalFile) {
151
            return $this->getThumbProfileUrl($attribute, $profile, $model);
152
        } elseif ($this->placeholder) {
153
            return $this->getPlaceholderUrl($profile);
154
        } else {
155
            return null;
156
        }
157
    }
158
159
    /**
160
     * @param $profile
161
     * @return string
162
     */
163 1
    protected function getPlaceholderUrl($profile)
164
    {
165 1
        list ($path, $url) = Yii::$app->assetManager->publish($this->placeholder);
166 1
        $filename = basename($path);
167 1
        $thumb = $this->getThumbFileName($filename, $profile);
168 1
        $thumbPath = dirname($path) . DIRECTORY_SEPARATOR . $thumb;
169 1
        $thumbUrl = dirname($url) . '/' . $thumb;
170
171 1
        if (!is_file($thumbPath)) {
172 1
            $this->generateImageThumb($this->thumbs[$profile], $path, $thumbPath);
173
        }
174
175 1
        return $thumbUrl;
176
    }
177
178
    /**
179
     * @param $attribute
180
     * @param $profile
181
     * @param BaseActiveRecord $model
182
     * @return bool|string
183
     */
184 2
    protected function getThumbProfileUrl($attribute, $profile, BaseActiveRecord $model)
185
    {
186 2
        $url = $this->resolvePath($this->thumbUrl);
187 2
        $fileName = $model->getOldAttribute($attribute);
188 2
        $thumbName = $this->getThumbFileName($fileName, $profile);
189
190 2
        return Yii::getAlias($url . '/' . $thumbName);
191
    }
192
193
    /**
194
     * @inheritdoc
195
     */
196 3
    protected function afterUpload()
197
    {
198 3
        parent::afterUpload();
199 3
        if ($this->createThumbsOnSave) {
200
            $this->createThumbs();
201
        }
202 3
    }
203
204
    /**
205
     * @param string $needed_profile - profile name to create thumb
206
     * @throws \yii\base\Exception
207
     * @throws \yii\base\InvalidConfigException
208
     */
209 2
    protected function createThumbs($needed_profile = false)
210
    {
211 2
        $path = $this->getUploadPath($this->attribute);
212 2
        foreach ($this->thumbs as $profile => $config) {
213
            //skip profiles not needed now
214 2
            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...
215 2
                continue;
216
            }
217
218 2
            $thumbPath = $this->getThumbUploadPath($this->attribute, $profile);
219 2
            if ($thumbPath !== null) {
220 2
                if (!FileHelper::createDirectory(dirname($thumbPath))) {
221
                    throw new InvalidArgumentException(
222
                        "Directory specified in 'thumbPath' attribute doesn't exist or cannot be created."
223
                    );
224
                }
225 2
                if (!is_file($thumbPath) && !file_exists($thumbPath)) {
226 2
                    $this->generateImageThumb($config, $path, $thumbPath);
227
                }
228
            }
229
        }
230
231 2
        if ($this->deleteOriginalFile) {
232
            $this->deleteFile($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->getUploadPath($this->attribute) on line 211 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...
233
        }
234 2
    }
235
236
    /**
237
     * @param string $attribute
238
     * @param string $profile
239
     * @param boolean $old
240
     * @return string
241
     * @throws \yii\base\InvalidConfigException
242
     */
243 2
    public function getThumbUploadPath($attribute, $profile = 'thumb', $old = false)
244
    {
245
        /** @var BaseActiveRecord $model */
246 2
        $model = $this->owner;
247 2
        $path = $this->resolvePath($this->thumbPath);
248 2
        $attribute = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
249 2
        $filename = $this->getThumbFileName($attribute, $profile);
250
251 2
        return $filename ? Yii::getAlias($path . '/' . $filename) : null;
252
    }
253
254
    /**
255
     * @param $filename
256
     * @param string $profile
257
     * @return string
258
     */
259 3
    protected function getThumbFileName($filename, $profile = 'thumb')
260
    {
261 3
        return $profile . '-' . $filename;
262
    }
263
264
    /**
265
     * @param $config
266
     * @param $path
267
     * @param $thumbPath
268
     */
269 3
    protected function generateImageThumb($config, $path, $thumbPath)
270
    {
271 3
        $width = ArrayHelper::getValue($config, 'width');
272 3
        $height = ArrayHelper::getValue($config, 'height');
273 3
        $quality = ArrayHelper::getValue($config, 'quality', 100);
274 3
        $mode = ArrayHelper::getValue($config, 'mode', ManipulatorInterface::THUMBNAIL_INSET);
275 3
        $bg_color = ArrayHelper::getValue($config, 'bg_color', 'FFF');
276
277 3
        if (!$width || !$height) {
278 2
            $image = Image::getImagine()->open($path);
279 2
            $ratio = $image->getSize()->getWidth() / $image->getSize()->getHeight();
280 2
            if ($width) {
281 2
                $height = ceil($width / $ratio);
282
            } else {
283
                $width = ceil($height * $ratio);
284
            }
285
        }
286
287
        // Fix error "PHP GD Allowed memory size exhausted".
288 3
        ini_set('memory_limit', '512M');
289
        //for big images size
290 3
        ini_set('max_execution_time', 60);
291 3
        Image::$thumbnailBackgroundColor = $bg_color;
292 3
        Image::thumbnail($path, $width, $height, $mode)->save($thumbPath, ['quality' => $quality]);
293 3
    }
294
295
    /**
296
     * @inheritdoc
297
     */
298 1
    protected function delete($attribute, $old = false)
299
    {
300 1
        $profiles = array_keys($this->thumbs);
301 1
        foreach ($profiles as $profile) {
302 1
            $path = $this->getThumbUploadPath($attribute, $profile, $old);
303 1
            $this->deleteFile($path);
0 ignored issues
show
Bug introduced by
It seems like $path defined by $this->getThumbUploadPat...ribute, $profile, $old) on line 302 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...
304
        }
305 1
        parent::delete($attribute, $old);
306 1
    }
307
}
308