Behavior::checkAttrExists()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

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