Completed
Push — master ( 20243b...b56016 )
by Pavel
03:58
created

ImageBehavior::afterSave()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 3
cts 3
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 1
1
<?php
2
/**
3
 * Image behavior for ActiveRecord
4
 *
5
 * @link https://github.com/inblank/yii2-image
6
 * @copyright Copyright (c) 2016 Pavel Aleksandrov <[email protected]>
7
 * @license http://opensource.org/licenses/MIT
8
 */
9
10
namespace inblank\image;
11
12
use Imagine\Filter\Advanced\Canvas;
13
use Imagine\Filter\Transformation;
14
use Imagine\Image\Box;
15
use Imagine\Image\Color;
16
use Imagine\Image\ImageInterface;
17
use Imagine\Image\Point;
18
use yii;
19
use yii\base\Behavior;
20
use yii\db\ActiveRecord;
21
22
/**
23
 * Class Image
24
 *
25
 * @property ActiveRecord $owner
26
 */
27
class ImageBehavior extends Behavior
28
{
29
    /** Just proportional resize image to fit size */
30
    const NONE = 0;
31
    /** Crop image resize strategy */
32
    const CROP = 1;
33
    /** Add frame image resize strategy */
34
    const FRAME = 2;
35
    /** Transparent color */
36
    const COLOR_TRANSPARENT = null;
37
    /**
38
     * Name of attribute to store the image
39
     * @var string
40
     */
41
    public $imageAttribute = "image";
42
    /**
43
     * Default image name
44
     * @var string
45
     */
46
    public $imageDefault = "image.png";
47
    /**
48
     * Path to store image files.
49
     * If empty will be init to /images/<ActiveRecord Class Name>
50
     * @var string
51
     */
52
    public $imagePath;
53
    /**
54
     * Size to convert image
55
     * If array: [width, height].
56
     * If integer: use value for with and height.
57
     * If not set: image save as is.
58
     * @var integer|array
59
     */
60
    public $imageSize;
61
    /**
62
     * Image resize strategy
63
     * @var int
64
     */
65
    public $imageResizeStrategy = self::NONE;
66
    /**
67
     * Image frame color
68
     * @var
69
     */
70
    public $imageFrameColor = "#FFF";
71
    /**
72
     * Calculated absolute image path relative to webroot
73
     * @var
74
     */
75
    protected $_imageAbsolutePath;
76
77
    /**
78
     * @inheritdoc
79
     */
80 3
    public function events()
81
    {
82
        return [
83 3
            ActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
84 3
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
85 3
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
86 3
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
87
        ];
88
    }
89
90
    /**
91
     * Before update action
92
     */
93
    public function beforeUpdate()
94
    {
95
        // prevent save empty image name value and also for correct remove old image
96
        $this->owner->setAttribute($this->imageAttribute, $this->owner->getOldAttribute($this->imageAttribute));
97
    }
98
99
    /**
100
     * After save action
101
     */
102 2
    public function afterSave()
103
    {
104 2
        $this->imageChangeByUpload();
105 2
    }
106
107
    /**
108
     * After delete action
109
     */
110 1
    public function afterDelete()
111
    {
112 1
        $this->imageRemoveFile();
113 1
    }
114
115
    /**
116
     * Get image path
117
     * @return string
118
     */
119 3
    public function getImagePath()
120
    {
121 3
        if ($this->imagePath === null) {
122 3
            $this->imagePath = '/images/' . strtolower((new \ReflectionClass($this->owner))->getShortName());
123
        }
124 3
        return $this->imagePath;
125
    }
126
127
    /**
128
     * Get default image URL
129
     * @return string
130
     * @throws yii\base\InvalidConfigException
131
     */
132
    public function getImageDefaultUrl()
133
    {
134
        return $this->_imageUrl($this->imageDefault);
135
    }
136
137
    /**
138
     * Get absolute team image path in filesystem
139
     * @return string
140
     */
141 3
    public function getImageAbsolutePath()
142
    {
143 3
        if ($this->_imageAbsolutePath === null) {
144 3
            $this->_imageAbsolutePath = Yii::getAlias('@webroot') .
145 3
                '/' . (defined('IS_BACKEND') ? '../' : '') .
146 3
                ltrim($this->getImagePath(), '/');
147 3
            if (!file_exists($this->_imageAbsolutePath)) {
148
                yii\helpers\FileHelper::createDirectory($this->_imageAbsolutePath);
149
            }
150
        }
151 3
        return $this->_imageAbsolutePath;
152
    }
153
154
    /**
155
     * Check team image
156
     * @return bool
157
     * @throws yii\base\InvalidConfigException
158
     */
159 3
    public function hasImage()
160
    {
161 3
        $image = $this->owner->getAttribute($this->imageAttribute);
162 3
        return !empty($image) && $image != $this->imageDefault;
163
    }
164
165
    /**
166
     * Check that image file exists
167
     */
168
    public function imageFileExists()
169
    {
170
        return file_exists($this->getImageFile());
171
    }
172
173
    /**
174
     * Get team image URL
175
     * @return string
176
     * @throws yii\base\InvalidConfigException
177
     */
178
    public function getImageUrl()
179
    {
180
        return !$this->hasImage() ?
181
            $this->getImageDefaultUrl() :
182
            $this->_imageUrl($this->owner->getAttribute($this->imageAttribute));
183
    }
184
185
    /**
186
     * Return filename in filesystem
187
     * @return string
188
     */
189 3
    public function getImageFile()
190
    {
191 3
        return $this->getImageAbsolutePath() . '/' . $this->owner->getAttribute($this->imageAttribute);
192
    }
193
194
    /**
195
     * Change image by uploaded file
196
     */
197 2
    public function imageChangeByUpload()
0 ignored issues
show
Coding Style introduced by
imageChangeByUpload uses the super-global variable $_FILES which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
198
    {
199 2
        $formName = $this->owner->formName();
200 2
        if (!empty($_FILES[$formName]['tmp_name'][$this->imageAttribute])) {
201
            $this->imageChange(
202
                [$_FILES[$formName]['name'][$this->imageAttribute] => $_FILES[$formName]['tmp_name'][$this->imageAttribute]]
203
            );
204
        }
205 2
    }
206
207
    /**
208
     * Change image
209
     * @param string|array $sourceFile source file. if set as array ['fileName' => 'file_in_filesystem']
210
     * @return bool true, if image was changed and old image file was deleted. false, if image not changed
211
     * @throws yii\base\InvalidConfigException
212
     */
213 3
    public function imageChange($sourceFile)
214
    {
215 3
        if (is_array($sourceFile)) {
216
            $fileName = key($sourceFile);
217
            $sourceFile = current($sourceFile);
218
        } else {
219 3
            $fileName = $sourceFile;
220
        }
221 3
        if (!file_exists($sourceFile)) {
222 1
            return false;
223
        }
224 3
        $imageName = $this->imageAttribute . '_' .
225 3
            md5(implode('-', (array)$this->owner->getPrimaryKey()) . microtime(true) . rand()) .
226 3
            '.' . pathinfo($fileName)['extension'];
227 3
        $destinationFile = $this->getImageAbsolutePath() . '/' . $imageName;
228 3
        if (!copy($sourceFile, $destinationFile)) {
229
            return false;
230
        }
231 3
        $this->imageRemoveFile();
232 3
        if (!empty($this->imageSize)) {
233 1
            $size = $this->imageSize;
234 1
            if (!is_array($size)) {
235 1
                $size = [$size, $size];
236
            }
237 1
            $newBox = new Box($size[0], $size[1]);
238 1
            $image = (new Transformation())->thumbnail(
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Imagine\Image\ManipulatorInterface as the method apply() does only exist in the following implementations of said interface: Imagine\Filter\Transformation.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
239
                $newBox,
240 1
                $this->imageResizeStrategy == self::CROP ?
241 1
                    ImageInterface::THUMBNAIL_OUTBOUND : ImageInterface::THUMBNAIL_INSET
242 1
            )->apply(yii\imagine\Image::getImagine()->open($destinationFile));
243 1
            $currentBox = $image->getSize();
244 1
            if ($this->imageResizeStrategy === self::FRAME) {
245
                if ($this->imageFrameColor === self::COLOR_TRANSPARENT) {
246
                    $frameColor = '#fff';
247
                    $alpha = 100;
248
                } else {
249
                    $frameColor = $this->imageFrameColor;
250
                    $alpha = 0;
251
                }
252
                $image = (new Transformation())->add(
253
                    new Canvas(
254
                        yii\imagine\Image::getImagine(),
255
                        $newBox,
256
                        $size[0] == $currentBox->getWidth() ?
257
                            new Point(0, ($size[1] - $currentBox->getHeight()) / 2) :
258
                            new Point(($size[0] - $currentBox->getWidth()) / 2, 0),
259
                        new Color($frameColor, $alpha)
260
                    )
261
                )->apply($image);
262
            }
263 1
            $image->save($destinationFile);
264
        }
265 3
        $this->owner->setAttribute($this->imageAttribute, $imageName);
266 3
        $this->owner->updateAttributes([$this->imageAttribute]);
267 3
        return true;
268
    }
269
270
    /**
271
     * Reset image to default
272
     * @throws yii\base\InvalidConfigException
273
     */
274 1
    public function imageReset()
275
    {
276 1
        $this->imageRemoveFile();
277 1
        $this->owner->setAttribute($this->imageAttribute, null);
278 1
        $this->owner->updateAttributes([$this->imageAttribute]);
279 1
    }
280
281
    /**
282
     * Remove current image
283
     * @throws yii\base\InvalidConfigException
284
     */
285 3
    protected function imageRemoveFile()
286
    {
287 3
        if ($this->hasImage()) {
288 2
            @unlink($this->getImageFile());
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
289
        }
290 3
    }
291
292
    /**
293
     * Make image URL
294
     * @param string $imageFileName image filename
295
     * @return string
296
     * @throws yii\base\InvalidConfigException
297
     */
298
    protected function _imageUrl($imageFileName)
299
    {
300
        return $this->getImagePath() . '/' . $imageFileName;
301
    }
302
303
}
304