Completed
Push — master ( 4542c8...20243b )
by Pavel
10:26
created

ImageBehavior::getImageAbsolutePath()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 4.0312

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 7
cts 8
cp 0.875
rs 9.2
c 0
b 0
f 0
cc 4
eloc 8
nc 5
nop 0
crap 4.0312
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
    /**
36
     * Name of attribute to store the image
37
     * @var string
38
     */
39
    public $imageAttribute = "image";
40
    /**
41
     * Default image name
42
     * @var string
43
     */
44
    public $imageDefault = "image.png";
45
    /**
46
     * Path to store image files.
47
     * If empty will be init to /images/<ActiveRecord Class Name>
48
     * @var string
49
     */
50
    public $imagePath;
51
    /**
52
     * Size to convert image
53
     * If array: [width, height].
54
     * If integer: use value for with and height.
55
     * If not set: image save as is.
56
     * @var integer|array
57
     */
58
    public $imageSize;
59
    /**
60
     * Image resize strategy
61
     * @var int
62
     */
63
    public $imageResizeStrategy = self::NONE;
64
    /**
65
     * Image frame color
66
     * @var
67
     */
68
    public $imageFrameColor = "#FFF";
69
    /**
70
     * Calculated absolute image path relative to webroot
71
     * @var
72
     */
73
    protected $_imageAbsolutePath;
74
75
    /**
76
     * @inheritdoc
77
     */
78 3
    public function events()
79
    {
80
        return [
81 3
            ActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
82 3
            ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
83 3
            ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
84 3
            ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeUpdate',
85
        ];
86
    }
87
88
    /**
89
     * Before update action
90
     */
91
    public function beforeUpdate()
92
    {
93
        // prevent save empty image name value and also for correct remove old image
94
        $this->owner->setAttribute($this->imageAttribute, $this->owner->getOldAttribute($this->imageAttribute));
95
    }
96
97
    /**
98
     * After save action
99
     */
100 2
    public function afterSave()
101
    {
102 2
        $this->imageChangeByUpload();
103 2
    }
104
105
    /**
106
     * After delete action
107
     */
108 1
    public function afterDelete()
109
    {
110 1
        $this->imageRemoveFile();
111 1
    }
112
113
    /**
114
     * Get image path
115
     * @return string
116
     */
117 3
    public function getImagePath()
118
    {
119 3
        if ($this->imagePath === null) {
120 3
            $this->imagePath = '/images/' . strtolower((new \ReflectionClass($this->owner))->getShortName());
121
        }
122 3
        return $this->imagePath;
123
    }
124
125
    /**
126
     * Get default image URL
127
     * @return string
128
     * @throws yii\base\InvalidConfigException
129
     */
130
    public function getImageDefaultUrl()
131
    {
132
        return $this->_imageUrl($this->imageDefault);
133
    }
134
135
    /**
136
     * Get absolute team image path in filesystem
137
     * @return string
138
     */
139 3
    public function getImageAbsolutePath()
140
    {
141 3
        if ($this->_imageAbsolutePath === null) {
142 3
            $this->_imageAbsolutePath = Yii::getAlias('@webroot') .
143 3
                '/' . (defined('IS_BACKEND') ? '../' : '') .
144 3
                ltrim($this->getImagePath(), '/');
145 3
            if (!file_exists($this->_imageAbsolutePath)) {
146
                yii\helpers\FileHelper::createDirectory($this->_imageAbsolutePath);
147
            }
148
        }
149 3
        return $this->_imageAbsolutePath;
150
    }
151
152
    /**
153
     * Check team image
154
     * @return bool
155
     * @throws yii\base\InvalidConfigException
156
     */
157 3
    public function hasImage()
158
    {
159 3
        $image = $this->owner->getAttribute($this->imageAttribute);
160 3
        return !empty($image) && $image != $this->imageDefault;
161
    }
162
163
    /**
164
     * Check that image file exists
165
     */
166
    public function imageFileExists()
167
    {
168
        return file_exists($this->getImageFile());
169
    }
170
171
    /**
172
     * Get team image URL
173
     * @return string
174
     * @throws yii\base\InvalidConfigException
175
     */
176
    public function getImageUrl()
177
    {
178
        return !$this->hasImage() ?
179
            $this->getImageDefaultUrl() :
180
            $this->_imageUrl($this->owner->getAttribute($this->imageAttribute));
181
    }
182
183
    /**
184
     * Return filename in filesystem
185
     * @return string
186
     */
187 3
    public function getImageFile()
188
    {
189 3
        return $this->getImageAbsolutePath() . '/' . $this->owner->getAttribute($this->imageAttribute);
190
    }
191
192
    /**
193
     * Change image by uploaded file
194
     */
195 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...
196
    {
197 2
        $formName = $this->owner->formName();
198 2
        if (!empty($_FILES[$formName]['tmp_name'][$this->imageAttribute])) {
199
            $this->imageChange(
200
                [$_FILES[$formName]['name'][$this->imageAttribute] => $_FILES[$formName]['tmp_name'][$this->imageAttribute]]
201
            );
202
        }
203 2
    }
204
205
    /**
206
     * Change image
207
     * @param string|array $sourceFile source file. if set as array ['fileName' => 'file_in_filesystem']
208
     * @return bool true, if image was changed and old image file was deleted. false, if image not changed
209
     * @throws yii\base\InvalidConfigException
210
     */
211 3
    public function imageChange($sourceFile)
212
    {
213 3
        if (is_array($sourceFile)) {
214
            $fileName = key($sourceFile);
215
            $sourceFile = current($sourceFile);
216
        } else {
217 3
            $fileName = $sourceFile;
218
        }
219 3
        if (!file_exists($sourceFile)) {
220 1
            return false;
221
        }
222 3
        $imageName = $this->imageAttribute . '_' .
223 3
            md5(implode('-', (array)$this->owner->getPrimaryKey()) . microtime(true) . rand()) .
224 3
            '.' . pathinfo($fileName)['extension'];
225 3
        $destinationFile = $this->getImageAbsolutePath() . '/' . $imageName;
226 3
        if (!copy($sourceFile, $destinationFile)) {
227
            return false;
228
        }
229 3
        $this->imageRemoveFile();
230 3
        if (!empty($this->imageSize)) {
231 1
            $size = $this->imageSize;
232 1
            if (!is_array($size)) {
233 1
                $size = [$size, $size];
234
            }
235 1
            $newBox = new Box($size[0], $size[1]);
236 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...
237
                $newBox,
238 1
                $this->imageResizeStrategy == self::CROP ?
239 1
                    ImageInterface::THUMBNAIL_OUTBOUND : ImageInterface::THUMBNAIL_INSET
240 1
            )->apply(yii\imagine\Image::getImagine()->open($destinationFile));
241 1
            $currentBox = $image->getSize();
242 1
            if ($this->imageResizeStrategy === self::FRAME) {
243
                $image = (new Transformation())->add(
244
                    new Canvas(
245
                        yii\imagine\Image::getImagine(),
246
                        $newBox,
247
                        $size[0] == $currentBox->getWidth() ?
248
                            new Point(0, ($size[1] - $currentBox->getHeight()) / 2) :
249
                            new Point(($size[0] - $currentBox->getWidth()) / 2, 0),
250
                        new Color($this->imageFrameColor)
251
                    )
252
                )->apply($image);
253
            }
254 1
            $image->save($destinationFile);
255
        }
256 3
        $this->owner->setAttribute($this->imageAttribute, $imageName);
257 3
        $this->owner->updateAttributes([$this->imageAttribute]);
258 3
        return true;
259
    }
260
261
    /**
262
     * Reset image to default
263
     * @throws yii\base\InvalidConfigException
264
     */
265 1
    public function imageReset()
266
    {
267 1
        $this->imageRemoveFile();
268 1
        $this->owner->setAttribute($this->imageAttribute, null);
269 1
        $this->owner->updateAttributes([$this->imageAttribute]);
270 1
    }
271
272
    /**
273
     * Remove current image
274
     * @throws yii\base\InvalidConfigException
275
     */
276 3
    protected function imageRemoveFile()
277
    {
278 3
        if ($this->hasImage()) {
279 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...
280
        }
281 3
    }
282
283
    /**
284
     * Make image URL
285
     * @param string $imageFileName image filename
286
     * @return string
287
     * @throws yii\base\InvalidConfigException
288
     */
289
    protected function _imageUrl($imageFileName)
290
    {
291
        return $this->getImagePath() . '/' . $imageFileName;
292
    }
293
294
}
295