Completed
Push — master ( 0d1833...676fbf )
by Alexander
02:03
created

UploadBehavior::init()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 4.5924
Metric Value
dl 0
loc 14
ccs 6
cts 9
cp 0.6667
rs 9.2
cc 4
eloc 8
nc 4
nop 0
crap 4.5924
1
<?php
2
3
namespace mongosoft\file;
4
5
use Closure;
6
use Yii;
7
use yii\base\Behavior;
8
use yii\base\InvalidConfigException;
9
use yii\base\InvalidParamException;
10
use yii\db\BaseActiveRecord;
11
use yii\helpers\ArrayHelper;
12
use yii\helpers\FileHelper;
13
use yii\web\UploadedFile;
14
15
/**
16
 * UploadBehavior automatically uploads file and fills the specified attribute
17
 * with a value of the name of the uploaded file.
18
 *
19
 * To use UploadBehavior, insert the following code to your ActiveRecord class:
20
 *
21
 * ```php
22
 * use mongosoft\file\UploadBehavior;
23
 *
24
 * function behaviors()
25
 * {
26
 *     return [
27
 *         [
28
 *             'class' => UploadBehavior::className(),
29
 *             'attribute' => 'file',
30
 *             'scenarios' => ['insert', 'update'],
31
 *             'path' => '@webroot/upload/{id}',
32
 *             'url' => '@web/upload/{id}',
33
 *         ],
34
 *     ];
35
 * }
36
 * ```
37
 *
38
 * @author Alexander Mohorev <[email protected]>
39
 */
40
class UploadBehavior extends Behavior
41
{
42
    /**
43
     * @event Event an event that is triggered after a file is uploaded.
44
     */
45
    const EVENT_AFTER_UPLOAD = 'afterUpload';
46
47
    /**
48
     * @var string the attribute which holds the attachment.
49
     */
50
    public $attribute;
51
    /**
52
     * @var array the scenarios in which the behavior will be triggered
53
     */
54
    public $scenarios = [];
55
    /**
56
     * @var string the base path or path alias to the directory in which to save files.
57
     */
58
    public $path;
59
    /**
60
     * @var string the base URL or path alias for this file
61
     */
62
    public $url;
63
    /**
64
     * @var bool Getting file instance by name
65
     */
66
    public $instanceByName = false;
67
    /**
68
     * @var boolean|callable generate a new unique name for the file
69
     * set true or anonymous function takes the old filename and returns a new name.
70
     * @see self::generateFileName()
71
     */
72
    public $generateNewName = true;
73
    /**
74
     * @var boolean If `true` current attribute file will be deleted
75
     */
76
    public $unlinkOnSave = true;
77
    /**
78
     * @var boolean If `true` current attribute file will be deleted after model deletion.
79
     */
80
    public $unlinkOnDelete = true;
81
    /**
82
     * @var boolean $deleteTempFile whether to delete the temporary file after saving.
83
     */
84
    public $deleteTempFile = true;
85
86
    /**
87
     * @var UploadedFile the uploaded file instance.
88
     */
89
    private $_file;
90
91
92
    /**
93
     * @inheritdoc
94
     */
95 7
    public function init()
96
    {
97 7
        parent::init();
98
99 7
        if ($this->attribute === null) {
100
            throw new InvalidConfigException('The "attribute" property must be set.');
101
        }
102 7
        if ($this->path === null) {
103
            throw new InvalidConfigException('The "path" property must be set.');
104
        }
105 7
        if ($this->url === null) {
106
            throw new InvalidConfigException('The "url" property must be set.');
107
        }
108 7
    }
109
110
    /**
111
     * @inheritdoc
112
     */
113 7
    public function events()
114
    {
115
        return [
116 7
            BaseActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate',
117 7
            BaseActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave',
118 7
            BaseActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave',
119 7
            BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
120 7
            BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
121 7
            BaseActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
122 7
        ];
123
    }
124
125
    /**
126
     * This method is invoked before validation starts.
127
     */
128 4
    public function beforeValidate()
129
    {
130
        /** @var BaseActiveRecord $model */
131 4
        $model = $this->owner;
132 4
        if (in_array($model->scenario, $this->scenarios)) {
133 4
            if (($file = $model->getAttribute($this->attribute)) instanceof UploadedFile) {
134 1
                $this->_file = $file;
135 1
            } else {
136 3
                if ($this->instanceByName === true) {
137
                    $this->_file = UploadedFile::getInstanceByName($this->attribute);
138
                } else {
139 3
                    $this->_file = UploadedFile::getInstance($model, $this->attribute);
140
                }
141
            }
142 4
            if ($this->_file instanceof UploadedFile) {
143 4
                $this->_file->name = $this->getFileName($this->_file);
144 4
                $model->setAttribute($this->attribute, $this->_file);
145 4
            }
146 4
        }
147 4
    }
148
149
    /**
150
     * This method is called at the beginning of inserting or updating a record.
151
     */
152 4
    public function beforeSave()
153
    {
154
        /** @var BaseActiveRecord $model */
155 4
        $model = $this->owner;
156 4
        if (in_array($model->scenario, $this->scenarios)) {
157 4
            if ($this->_file instanceof UploadedFile) {
158 4
                if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
159 1
                    if ($this->unlinkOnSave === true) {
160 1
                        $this->delete($this->attribute, true);
161 1
                    }
162 1
                }
163 4
                $model->setAttribute($this->attribute, $this->_file->name);
164 4
            } else {
165
                // Protect attribute
166
                unset($model->{$this->attribute});
167
            }
168 4
        } else {
169
            if (!$model->getIsNewRecord() && $model->isAttributeChanged($this->attribute)) {
170
                if ($this->unlinkOnSave === true) {
171
                    $this->delete($this->attribute, true);
172
                }
173
            }
174
        }
175 4
    }
176
177
    /**
178
     * This method is called at the end of inserting or updating a record.
179
     * @throws \yii\base\InvalidParamException
180
     */
181 4
    public function afterSave()
182
    {
183 4
        if ($this->_file instanceof UploadedFile) {
184 4
            $path = $this->getUploadPath($this->attribute);
185 4
            if (is_string($path) && FileHelper::createDirectory(dirname($path))) {
186 4
                $this->save($this->_file, $path);
187 4
                $this->afterUpload();
188 4
            } else {
189
                throw new InvalidParamException("Directory specified in 'path' attribute doesn't exist or cannot be created.");
190
            }
191 4
        }
192 4
    }
193
194
    /**
195
     * This method is invoked before deleting a record.
196
     */
197 4
    public function beforeDelete()
198
    {
199
        $attribute = $this->attribute;
200
        if ($this->unlinkOnDelete && $attribute) {
201
            $this->delete($attribute);
202 4
        }
203
    }
204
205
    /**
206
     * Returns file path for the attribute.
207
     * @param string $attribute
208
     * @param boolean $old
209
     * @return string|null the file path.
210
     */
211 4
    public function getUploadPath($attribute, $old = false)
212
    {
213
        /** @var BaseActiveRecord $model */
214 4
        $model = $this->owner;
215 4
        $path = $this->resolvePath($this->path);
216 4
        $fileName = ($old === true) ? $model->getOldAttribute($attribute) : $model->$attribute;
217
218 4
        return $fileName ? Yii::getAlias($path . '/' . $fileName) : null;
219
    }
220
221
    /**
222
     * Returns file url for the attribute.
223
     * @param string $attribute
224
     * @return string|null
225
     */
226
    public function getUploadUrl($attribute)
227
    {
228
        /** @var BaseActiveRecord $model */
229
        $model = $this->owner;
230
        $url = $this->resolvePath($this->url);
231
        $fileName = $model->getOldAttribute($attribute);
232
233
        return $fileName ? Yii::getAlias($url . '/' . $fileName) : null;
234
    }
235
236
    /**
237
     * Replaces all placeholders in path variable with corresponding values.
238
     */
239 4
    protected function resolvePath($path)
240
    {
241
        /** @var BaseActiveRecord $model */
242 4
        $model = $this->owner;
243 4
        return preg_replace_callback('/{([^}]+)}/', function ($matches) use ($model) {
244 4
            $name = $matches[1];
245 4
            $attribute = ArrayHelper::getValue($model, $name);
246 4
            if (is_string($attribute) || is_numeric($attribute)) {
247 4
                return $attribute;
248
            } else {
249
                return $matches[0];
250
            }
251 4
        }, $path);
252
    }
253
254
    /**
255
     * Saves the uploaded file.
256
     * @param UploadedFile $file the uploaded file instance
257
     * @param string $path the file path used to save the uploaded file
258
     * @return boolean true whether the file is saved successfully
259
     */
260 4
    protected function save($file, $path)
261
    {
262 4
        return $file->saveAs($path, $this->deleteTempFile);
263
    }
264
265
    /**
266
     * Deletes old file.
267
     * @param string $attribute
268
     * @param boolean $old
269
     */
270 1
    protected function delete($attribute, $old = false)
271
    {
272 1
        $path = $this->getUploadPath($attribute, $old);
273 1
        if (is_file($path)) {
274
            unlink($path);
275
        }
276 1
    }
277
278
    /**
279
     * @param UploadedFile $file
280
     * @return string
281
     */
282 4
    protected function getFileName($file)
283
    {
284 4
        if ($this->generateNewName) {
285 4
            return $this->generateNewName instanceof Closure
286 4
                ? call_user_func($this->generateNewName, $file)
287 4
                : $this->generateFileName($file);
288
        } else {
289
            return $this->sanitize($file->name);
290
        }
291
    }
292
293
    /**
294
     * Replaces characters in strings that are illegal/unsafe for filename.
295
     *
296
     * #my*  unsaf<e>&file:name?".png
297
     *
298
     * @param string $filename the source filename to be "sanitized"
299
     * @return boolean string the sanitized filename
300
     */
301 1
    public static function sanitize($filename)
302
    {
303 1
        return str_replace([' ', '"', '\'', '&', '/', '\\', '?', '#'], '-', $filename);
304
    }
305
306
    /**
307
     * Generates random filename.
308
     * @param UploadedFile $file
309
     * @return string
310
     */
311 4
    protected function generateFileName($file)
312
    {
313 4
        return uniqid() . '.' . $file->extension;
314
    }
315
316
    /**
317
     * This method is invoked after uploading a file.
318
     * The default implementation raises the [[EVENT_AFTER_UPLOAD]] event.
319
     * You may override this method to do postprocessing after the file is uploaded.
320
     * Make sure you call the parent implementation so that the event is raised properly.
321
     */
322 4
    protected function afterUpload()
323
    {
324 4
        $this->owner->trigger(self::EVENT_AFTER_UPLOAD);
325 4
    }
326
}
327