Completed
Push — master ( 771aec...df0d15 )
by Maxim
01:30
created

Behavior::crop()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 19
rs 9.0111
c 0
b 0
f 0
cc 6
nc 4
nop 3
1
<?php
2
3
namespace maxmirazh33\image;
4
5
use Imagine\Image\Box;
6
use Imagine\Image\ImageInterface;
7
use Imagine\Image\ManipulatorInterface;
8
use Imagine\Image\Point;
9
use yii\base\Exception;
10
use yii\base\InvalidArgumentException;
11
use yii\base\InvalidCallException;
12
use yii\base\InvalidConfigException;
13
use yii\db\ActiveRecord;
14
use yii\imagine\Image;
15
use yii\web\UploadedFile;
16
use yii\helpers\FileHelper;
17
use Yii;
18
19
/**
20
 * Class model behavior for uploadable and cropable image
21
 *
22
 * Usage in your model:
23
 * ```
24
 * ...
25
 * public function behaviors()
26
 * {
27
 *     return [
28
 *         [
29
 *              'class' => \maxmirazh33\image\Behavior::className(),
30
 *              'savePathAlias' => '@web/images/',
31
 *              'urlPrefix' => '/images/',
32
 *              'crop' => true,
33
 *              'attributes' => [
34
 *                  'avatar' => [
35
 *                      'savePathAlias' => '@web/images/avatars/',
36
 *                      'urlPrefix' => '/images/avatars/',
37
 *                      'width' => 100,
38
 *                      'height' => 100,
39
 *                  ],
40
 *                  'logo' => [
41
 *                      'crop' => false,
42
 *                      'thumbnails' => [
43
 *                          'mini' => [
44
 *                              'width' => 50,
45
 *                          ],
46
 *                      ],
47
 *                  ],
48
 *              ],
49
 *         ],
50
 *     //other behaviors
51
 *     ];
52
 * }
53
 * ...
54
 * ```
55
 * @property ActiveRecord $owner
56
 */
57
class Behavior extends \yii\base\Behavior
58
{
59
    /**
60
     * @var array list of attribute as attributeName => options. Options:
61
     *  $width image width
62
     *  $height image height
63
     *  $savePathAlias @see \maxmirazh33\image\Behavior::$savePathAlias
64
     *  $crop @see \maxmirazh33\image\Behavior::$crop
65
     *  $urlPrefix @see \maxmirazh33\image\Behavior::$urlPrefix
66
     *  $thumbnails - array of thumbnails as prefix => options. Options:
67
     *          $width thumbnail width
68
     *          $height thumbnail height
69
     *          $savePathAlias @see \maxmirazh33\image\Behavior::$savePathAlias
70
     *          $urlPrefix @see \maxmirazh33\image\Behavior::$urlPrefix
71
     */
72
    public $attributes = [];
73
    /**
74
     * @var string. Default '@frontend/web/images/%className%/' or '@app/web/images/%className%/'
75
     */
76
    public $savePathAlias;
77
    /**
78
     * @var bool enable/disable crop.
79
     */
80
    public $crop = true;
81
    /**
82
     * @var string part of url for image without hostname. Default '/images/%className%/'
83
     */
84
    public $urlPrefix;
85
86
    /**
87
     * @inheritdoc
88
     */
89
    public function events()
90
    {
91
        return [
92
            ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
93
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
94
            ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
95
            ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
96
        ];
97
    }
98
99
    /**
100
     * function for EVENT_BEFORE_VALIDATE
101
     */
102
    public function beforeValidate()
103
    {
104
        $model = $this->owner;
105
        foreach ($this->attributes as $attr => $options) {
106
            self::ensureAttribute($attr, $options);
107
            if ($file = UploadedFile::getInstance($model, $attr)) {
108
                $model->{$attr} = $file;
109
            }
110
        }
111
    }
112
113
    /**
114
     * function for EVENT_BEFORE_INSERT and EVENT_BEFORE_UPDATE
115
     */
116
    public function beforeSave()
117
    {
118
        $model = $this->owner;
119
        foreach ($this->attributes as $attr => $options) {
120
            self::ensureAttribute($attr, $options);
121
            if ($file = UploadedFile::getInstance($model, $attr)) {
122
                $this->createDirIfNotExists($attr);
123
                if (!$model->isNewRecord) {
124
                    $this->deleteFiles($attr);
125
                }
126
                $fileName = uniqid('', true) . '.' . $file->extension;
127
                if ($this->needCrop($attr)) {
128
                    $coords = $this->getCoords($attr);
129
                    if ($coords === false) {
130
                        throw new InvalidCallException('invalid coords param');
131
                    }
132
                    $image = $this->crop($file, $coords, $options);
133
                    $image->save($this->getSavePath($attr) . $fileName);
134
                } else {
135
                    $image = $this->processImage($file->tempName, $options);
136
                    $image->save($this->getSavePath($attr) . $fileName);
137
                }
138
                $model->{$attr} = $fileName;
139
140
                if ($this->issetThumbnails($attr)) {
141
                    $thumbnails = $this->attributes[$attr]['thumbnails'];
142
                    foreach ($thumbnails as $name => $options) {
143
                        self::ensureAttribute($name, $options);
144
                        $tmbFileName = $name . '_' . $fileName;
145
                        $image = $this->processImage($this->getSavePath($attr) . $fileName, $options);
146
                        $image->save($this->getSavePath($attr) . $tmbFileName);
147
                    }
148
                }
149
            } elseif (isset($model->oldAttributes[$attr])) {
150
                $model->{$attr} = $model->oldAttributes[$attr];
151
            }
152
        }
153
    }
154
155
    /**
156
     * Crop image
157
     * @param UploadedFile $file
158
     * @param array $coords
159
     * @param array $options
160
     * @return ManipulatorInterface
161
     */
162
    private function crop($file, array $coords, array $options)
163
    {
164
        if (isset($options['width']) && !isset($options['height'])) {
165
            $width = $options['width'];
166
            $height = $options['width'] * $coords['h'] / $coords['w'];
167
        } elseif (!isset($options['width']) && isset($options['height'])) {
168
            $width = $options['height'] * $coords['w'] / $coords['h'];
169
            $height = $options['height'];
170
        } elseif (isset($options['width'], $options['height'])) {
171
            $width = $options['width'];
172
            $height = $options['height'];
173
        } else {
174
            $width = $coords['w'];
175
            $height = $coords['h'];
176
        }
177
178
        return Image::crop($file->tempName, $coords['w'], $coords['h'], [$coords['x'], $coords['y']])
179
            ->resize(new Box($width, $height));
180
    }
181
182
    /**
183
     * @param ActiveRecord $object
184
     * @return string
185
     * @throws \ReflectionException
186
     */
187
    private function getShortClassName($object)
188
    {
189
        $obj = new \ReflectionClass($object);
190
        return mb_strtolower($obj->getShortName());
191
    }
192
193
    /**
194
     * @param string $original path to original image
195
     * @param array $options with width and height
196
     * @return ImageInterface
197
     */
198
    private function processImage($original, $options)
199
    {
200
        list($imageWidth, $imageHeight) = getimagesize($original);
201
        $image = Image::getImagine()->open($original);
202
        if (isset($options['width']) && !isset($options['height'])) {
203
            $width = $options['width'];
204
            $height = $options['width'] * $imageHeight / $imageWidth;
205
            $image->resize(new Box($width, $height));
206
        } elseif (!isset($options['width']) && isset($options['height'])) {
207
            $width = $options['height'] * $imageWidth / $imageHeight;
208
            $height = $options['height'];
209
            $image->resize(new Box($width, $height));
210
        } elseif (isset($options['width'], $options['height'])) {
211
            $width = $options['width'];
212
            $height = $options['height'];
213
            if ($width / $height > $imageWidth / $imageHeight) {
214
                $resizeHeight = $width * $imageHeight / $imageWidth;
215
                $image->resize(new Box($width, $resizeHeight))
216
                    ->crop(new Point(0, ($resizeHeight - $height) / 2), new Box($width, $height));
217
            } else {
218
                $resizeWidth = $height * $imageWidth / $imageHeight;
219
                $image->resize(new Box($resizeWidth, $height))
220
                    ->crop(new Point(($resizeWidth - $width) / 2, 0), new Box($width, $height));
221
            }
222
        }
223
224
        return $image;
225
    }
226
227
    /**
228
     * @param string $attr name of attribute
229
     * @return bool need crop or not
230
     */
231
    public function needCrop($attr)
232
    {
233
        return isset($this->attributes[$attr]['crop']) ? $this->attributes[$attr]['crop'] : $this->crop;
234
    }
235
236
    /**
237
     * @param string $attr name of attribute
238
     * @return array|bool false if no coords and array if coords exists
239
     * @throws InvalidConfigException
240
     */
241
    private function getCoords($attr)
242
    {
243
        $post = Yii::$app->request->post($this->owner->formName());
244
        if ($post === null) {
245
            return false;
246
        }
247
        $x = $post[$attr . '-coords']['x'];
248
        $y = $post[$attr . '-coords']['y'];
249
        $w = $post[$attr . '-coords']['w'];
250
        $h = $post[$attr . '-coords']['h'];
251
        if (!isset($x, $y, $w, $h)) {
252
            return false;
253
        }
254
255
        return [
256
            'x' => $x,
257
            'y' => $y,
258
            'w' => $w,
259
            'h' => $h
260
        ];
261
    }
262
263
    /**
264
     * function for EVENT_BEFORE_DELETE
265
     */
266
    public function beforeDelete()
267
    {
268
        foreach ($this->attributes as $attr => $options) {
269
            self::ensureAttribute($attr, $options);
270
            $this->deleteFiles($attr);
271
        }
272
    }
273
274
    /**
275
     * @param string $attr name of attribute
276
     * @param bool|string $tmb false or name of thumbnail
277
     * @param ActiveRecord $object that keep attrribute. Default $this->owner
278
     * @return string url to image
279
     */
280
    public function getImageUrl($attr, $tmb = false, $object = null)
281
    {
282
        $this->checkAttrExists($attr);
283
        $prefix = $this->getUrlPrefix($attr, $tmb, $object);
284
285
        $object = isset($object) ? $object : $this->owner;
286
287
        if ($tmb) {
288
            return $prefix . $tmb . '_' . $object->{$attr};
289
        }
290
291
        return $prefix . $object->{$attr};
292
    }
293
294
    /**
295
     * @param string $attr name of attribute
296
     * @throws Exception
297
     */
298
    private function createDirIfNotExists($attr)
299
    {
300
        $dir = $this->getSavePath($attr);
301
        if (!is_dir($dir)) {
302
            FileHelper::createDirectory($dir);
303
        }
304
    }
305
306
    /**
307
     * @param string $attr name of attribute
308
     * @param bool|string $tmb name of thumbnail
309
     * @return string save path
310
     * @throws \ReflectionException
311
     */
312
    private function getSavePath($attr, $tmb = false)
313
    {
314
        if ($tmb !== false) {
315
            if (isset($this->attributes[$attr]['thumbnails'][$tmb]['savePathAlias'])) {
316
                return rtrim(Yii::getAlias($this->attributes[$attr]['thumbnails'][$tmb]['savePathAlias']), '\/') . DIRECTORY_SEPARATOR;
317
            }
318
        }
319
320
        if (isset($this->attributes[$attr]['savePathAlias'])) {
321
            return rtrim(Yii::getAlias($this->attributes[$attr]['savePathAlias']), '\/') . DIRECTORY_SEPARATOR;
322
        }
323
        if (isset($this->savePathAlias)) {
324
            return rtrim(Yii::getAlias($this->savePathAlias), '\/') . DIRECTORY_SEPARATOR;
325
        }
326
327
        if (isset(Yii::$aliases['@frontend'])) {
328
            return Yii::getAlias('@frontend/web/images/' . $this->getShortClassName($this->owner)) . DIRECTORY_SEPARATOR;
329
        }
330
331
        return Yii::getAlias('@app/web/images/' . $this->getShortClassName($this->owner)) . DIRECTORY_SEPARATOR;
332
    }
333
334
    /**
335
     * @param string $attr name of attribute
336
     * @param bool|string $tmb name of thumbnail
337
     * @param ActiveRecord $object for default prefix
338
     * @return string url prefix
339
     * @throws \ReflectionException
340
     */
341
    private function getUrlPrefix($attr, $tmb = false, $object = null)
342
    {
343
        if ($tmb !== false && isset($this->attributes[$attr]['thumbnails'][$tmb]['urlPrefix'])) {
344
            return '/' . trim($this->attributes[$attr]['thumbnails'][$tmb]['urlPrefix'], '/') . '/';
345
        }
346
347
        if (isset($this->attributes[$attr]['urlPrefix'])) {
348
            return '/' . trim($this->attributes[$attr]['urlPrefix'], '/') . '/';
349
        }
350
        if (isset($this->urlPrefix)) {
351
            return '/' . trim($this->urlPrefix, '/') . '/';
352
        }
353
354
        $object = isset($object) ? $object : $this->owner;
355
        return '/images/' . $this->getShortClassName($object) . '/';
356
    }
357
358
    /**
359
     * Delete images
360
     * @param string $attr name of attribute
361
     * @throws \ReflectionException
362
     */
363
    private function deleteFiles($attr)
364
    {
365
        $base = $this->getSavePath($attr);
366
        $model = $this->owner;
367
        if ($model->isNewRecord) {
368
            $value = $model->{$attr};
369
        } else {
370
            $value = $model->oldAttributes[$attr];
371
        }
372
        $file = $base . $value;
373
374
        if (is_file($file)) {
375
            unlink($file);
376
        }
377
        if ($this->issetThumbnails($attr)) {
378
            foreach ($this->attributes[$attr]['thumbnails'] as $name => $options) {
379
                self::ensureAttribute($name, $options);
380
                $file = $base . $name . '_' . $value;
381
                if (is_file($file)) {
382
                    unlink($file);
383
                }
384
            }
385
        }
386
    }
387
388
    /**
389
     * @param string $attr name of attribute
390
     * @return bool isset thumbnails or not
391
     */
392
    private function issetThumbnails($attr)
393
    {
394
        return isset($this->attributes[$attr]['thumbnails']) && is_array($this->attributes[$attr]['thumbnails']);
395
    }
396
397
    /**
398
     * Check, isset attribute or not
399
     * @param string $attribute name of attribute
400
     * @throws InvalidArgumentException
401
     */
402
    private function checkAttrExists($attribute)
403
    {
404
        foreach ($this->attributes as $attr => $options) {
405
            self::ensureAttribute($attr, $options);
406
            if ($attr == $attribute) {
407
                return;
408
            }
409
        }
410
        throw new InvalidArgumentException('checkAttrExists failed');
411
    }
412
413
    /**
414
     * @param $attr
415
     * @param $options
416
     */
417
    public static function ensureAttribute(&$attr, &$options)
418
    {
419
        if (!is_array($options)) {
420
            $attr = $options;
421
            $options = [];
422
        }
423
    }
424
}
425