Completed
Push — master ( e96430...d9c7d9 )
by Alex
02:11
created

UploadImageBehavior::init()   B

Complexity

Conditions 7
Paths 13

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 7.116

Importance

Changes 0
Metric Value
dl 0
loc 26
ccs 13
cts 15
cp 0.8667
rs 8.5706
c 0
b 0
f 0
cc 7
nc 13
nop 0
crap 7.116
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 8
                    '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 1
    public function getThumbUploadUrl($attribute, $profile = 'thumb')
129
    {
130
        /** @var BaseActiveRecord $model */
131 1
        $model = $this->owner;
132
133 1
        if ($this->attribute !== $attribute) {
134 1
            $behaviors = $model->getBehaviors();
135
136 1
            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 1
                if ($behavior instanceof UploadImageBehavior) {
138 1
                    if ($behavior->attribute == $attribute) {
139 1
                        return $behavior->getThumbUploadUrl($attribute, $profile);
140
                    }
141
                }
142
            }
143
        }
144
145 1
        if (!$model->getAttribute($attribute)) {
146 1
            if ($this->placeholder) {
147 1
                return $this->getPlaceholderUrl($profile);
148
            } else {
149
                return null;
150
            }
151
        }
152
153
        $path = $this->getUploadPath($attribute, true);
154
155
        //if original file exist - generate profile thumb and generate url to thumb
156
        if (is_file($path) || !$this->deleteOriginalFile) {
157
            if ($this->createThumbsOnRequest) {
158
                $this->createThumbs($profile);
159
            }
160
            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 1
    protected function getPlaceholderUrl($profile)
176
    {
177 1
        list ($path, $url) = Yii::$app->assetManager->publish($this->placeholder);
178
        $filename = basename($path);
179
        $thumb = $this->getThumbFileName($filename, $profile);
180
        $thumbPath = dirname($path) . DIRECTORY_SEPARATOR . $thumb;
181
        $thumbUrl = dirname($url) . '/' . $thumb;
182
183
        if (!is_file($thumbPath)) {
184
            $this->generateImageThumb($this->thumbs[$profile], $path, $thumbPath);
185
        }
186
187
        return $thumbUrl;
188
    }
189
190
    /**
191
     * @param $attribute
192
     * @param $profile
193
     * @param BaseActiveRecord $model
194
     * @return bool|string
195
     */
196
    protected function getThumbProfileUrl($attribute, $profile, BaseActiveRecord $model)
197
    {
198
        $url = $this->resolvePath($this->thumbUrl);
199
        $fileName = $model->getOldAttribute($attribute);
200
        $thumbName = $this->getThumbFileName($fileName, $profile);
201
202
        return Yii::getAlias($url . '/' . $thumbName);
203
    }
204
205
    /**
206
     * @inheritdoc
207
     */
208 1
    protected function afterUpload()
209
    {
210 1
        parent::afterUpload();
211 1
        if ($this->createThumbsOnSave) {
212
            $this->createThumbs();
213
        }
214 1
    }
215
216
    /**
217
     * @param string $needed_profile - profile name to create thumb
218
     * @throws \yii\base\Exception
219
     * @throws \yii\base\InvalidConfigException
220
     */
221
    protected function createThumbs($needed_profile = false)
222
    {
223
        $path = $this->getUploadPath($this->attribute);
224
        foreach ($this->thumbs as $profile => $config) {
225
            //skip profiles not needed now
226
            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
                continue;
228
            }
229
230
            $thumbPath = $this->getThumbUploadPath($this->attribute, $profile);
231
            if ($thumbPath !== null) {
232
                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
                if (!is_file($thumbPath) && !file_exists($thumbPath)) {
238
                    $this->generateImageThumb($config, $path, $thumbPath);
239
                }
240
            }
241
        }
242
243
        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
    }
247
248
    /**
249
     * @param string $attribute
250
     * @param string $profile
251
     * @param boolean $old
252
     * @return string
253
     * @throws \yii\base\InvalidConfigException
254
     */
255
    public function getThumbUploadPath($attribute, $profile = 'thumb', $old = false)
256
    {
257
        /** @var BaseActiveRecord $model */
258
        $model = $this->owner;
259
        $path = $this->resolvePath($this->thumbPath);
260
        $attribute = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
261
        $filename = $this->getThumbFileName($attribute, $profile);
262
263
        return $filename ? Yii::getAlias($path . '/' . $filename) : null;
264
    }
265
266
    /**
267
     * @param $filename
268
     * @param string $profile
269
     * @return string
270
     */
271
    protected function getThumbFileName($filename, $profile = 'thumb')
272
    {
273
        return $profile . '-' . $filename;
274
    }
275
276
    /**
277
     * @param $config
278
     * @param $path
279
     * @param $thumbPath
280
     */
281
    protected function generateImageThumb($config, $path, $thumbPath)
282
    {
283
        $width = ArrayHelper::getValue($config, 'width');
284
        $height = ArrayHelper::getValue($config, 'height');
285
        $quality = ArrayHelper::getValue($config, 'quality', 100);
286
        $mode = ArrayHelper::getValue($config, 'mode', ManipulatorInterface::THUMBNAIL_INSET);
287
        $bg_color = ArrayHelper::getValue($config, 'bg_color', 'FFF');
288
289
        if (!$width || !$height) {
290
            $image = Image::getImagine()->open($path);
291
            $ratio = $image->getSize()->getWidth() / $image->getSize()->getHeight();
292
            if ($width) {
293
                $height = ceil($width / $ratio);
294
            } else {
295
                $width = ceil($height * $ratio);
296
            }
297
        }
298
299
        // Fix error "PHP GD Allowed memory size exhausted".
300
        ini_set('memory_limit', '512M');
301
        //for big images size
302
        ini_set('max_execution_time', 60);
303
        Image::$thumbnailBackgroundColor = $bg_color;
304
        Image::thumbnail($path, $width, $height, $mode)->save($thumbPath, ['quality' => $quality]);
305
    }
306
307
    /**
308
     * @inheritdoc
309
     */
310
    protected function delete($attribute, $old = false)
311
    {
312
        $profiles = array_keys($this->thumbs);
313
        foreach ($profiles as $profile) {
314
            $path = $this->getThumbUploadPath($attribute, $profile, $old);
315
            $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
        parent::delete($attribute, $old);
318
    }
319
}
320