1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace yiicod\fileupload\models\behaviors; |
4
|
|
|
|
5
|
|
|
use Exception; |
6
|
|
|
use Symfony\Component\Filesystem\Filesystem; |
7
|
|
|
use Yii; |
8
|
|
|
use yii\base\Behavior; |
9
|
|
|
use yii\base\Event; |
10
|
|
|
use yii\db\ActiveRecord; |
11
|
|
|
use yii\helpers\FileHelper; |
12
|
|
|
use yiicod\fileupload\events\RemoveAllFiles; |
13
|
|
|
use yiicod\fileupload\events\RemoveFile; |
14
|
|
|
use yiicod\fileupload\helpers\FileUpload; |
15
|
|
|
|
16
|
|
|
/** |
17
|
|
|
* Coco behavior uploader |
18
|
|
|
* |
19
|
|
|
* @author Orlov Alexey <[email protected]> |
20
|
|
|
*/ |
21
|
|
|
class FileUploadBehavior extends Behavior |
22
|
|
|
{ |
23
|
|
|
/** |
24
|
|
|
* Events |
25
|
|
|
*/ |
26
|
|
|
const EVENT_REMOVE_FILE = 'removeFile'; |
27
|
|
|
const EVENT_REMOVE_ALL_FILES = 'removeAllFiles'; |
28
|
|
|
|
29
|
|
|
/** |
30
|
|
|
* File max name length |
31
|
|
|
* |
32
|
|
|
* @var int |
33
|
|
|
*/ |
34
|
|
|
public $maxLength = 50; |
35
|
|
|
|
36
|
|
|
/** |
37
|
|
|
* An array where keys are fields that contain file |
38
|
|
|
*/ |
39
|
|
|
public $fields = []; |
40
|
|
|
/** |
41
|
|
|
* mkdir mode |
42
|
|
|
* |
43
|
|
|
* @var int |
44
|
|
|
*/ |
45
|
|
|
public $mode = 0755; |
46
|
|
|
|
47
|
|
|
/** |
48
|
|
|
* @var array TmpRepositoryInterface config |
49
|
|
|
*/ |
50
|
|
|
public $tmpRepositoryClass = [ |
51
|
|
|
'class' => TmpRepository::class, |
52
|
|
|
]; |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* @var array TmpRepositoryInterface config |
56
|
|
|
*/ |
57
|
|
|
public $sourceRepositoryClass = [ |
58
|
|
|
'class' => SourceRepository::class, |
59
|
|
|
'uploadUrl' => '', |
60
|
|
|
'uploadDir' => '', |
61
|
|
|
]; |
62
|
|
|
|
63
|
|
|
/** |
64
|
|
|
* Prepared values |
65
|
|
|
* |
66
|
|
|
* @var array |
67
|
|
|
*/ |
68
|
|
|
protected $fieldsValues = []; |
69
|
|
|
|
70
|
|
|
/** |
71
|
|
|
* Origin values |
72
|
|
|
* |
73
|
|
|
* @var array |
74
|
|
|
*/ |
75
|
|
|
protected $originValues = []; |
76
|
|
|
|
77
|
|
|
/** |
78
|
|
|
* @var TmpRepositoryInterface |
79
|
|
|
*/ |
80
|
|
|
private $tmpRepository; |
81
|
|
|
|
82
|
|
|
/** |
83
|
|
|
* @var SourceRepositoryInterface |
84
|
|
|
*/ |
85
|
|
|
private $sourceRepository; |
86
|
|
|
|
87
|
|
|
/** |
88
|
|
|
* Set tmp file |
89
|
|
|
* |
90
|
|
|
* @param $fileName |
91
|
|
|
* @param $field |
92
|
|
|
* |
93
|
|
|
* @return bool |
94
|
|
|
*/ |
95
|
|
|
public function setTmpFile($filePath, $field): bool |
96
|
|
|
{ |
97
|
|
|
return $this->getTmpRepository()->setFile($filePath, $field); |
98
|
|
|
} |
99
|
|
|
|
100
|
|
|
/** |
101
|
|
|
* Get tmp file |
102
|
|
|
* |
103
|
|
|
* @param $field |
104
|
|
|
* |
105
|
|
|
* @return string |
106
|
|
|
*/ |
107
|
|
|
public function getTmpFile($field): string |
108
|
|
|
{ |
109
|
|
|
return $this->getTmpRepository()->getFile($field); |
110
|
|
|
} |
111
|
|
|
|
112
|
|
|
/** |
113
|
|
|
* Remove file by session key |
114
|
|
|
* |
115
|
|
|
* @param string $field |
116
|
|
|
* @param bool $fieldReset |
117
|
|
|
*/ |
118
|
|
|
public function removeTmpFile(string $field, bool $fieldReset = false) |
119
|
|
|
{ |
120
|
|
|
$this->getTmpRepository()->removeFile($field); |
121
|
|
|
if (true === $fieldReset) { |
122
|
|
|
$this->owner->{$field} = ''; |
123
|
|
|
} |
124
|
|
|
} |
125
|
|
|
|
126
|
|
|
/** |
127
|
|
|
* Remove field value and relative physical file |
128
|
|
|
* |
129
|
|
|
* @param string $field |
130
|
|
|
*/ |
131
|
|
|
public function removeFile(string $field) |
132
|
|
|
{ |
133
|
|
|
Event::trigger($this, self::EVENT_REMOVE_FILE, new RemoveFile($field, $this->getSourceRepository()->getUploadDir(), $this->getFilePath($field))); |
134
|
|
|
|
135
|
|
|
$this->owner->{$field} = ''; |
136
|
|
|
$this->owner->save(false); |
137
|
|
|
|
138
|
|
|
$fs = new Filesystem(); |
139
|
|
|
$fs->remove($this->getFilePath($field)); |
140
|
|
|
} |
141
|
|
|
|
142
|
|
|
/** |
143
|
|
|
* Remove all files relative to model |
144
|
|
|
*/ |
145
|
|
|
public function removeFiles() |
146
|
|
|
{ |
147
|
|
|
Event::trigger($this, self::EVENT_REMOVE_ALL_FILES, new RemoveAllFiles($this->getSourceRepository()->getUploadDir(), $this->getFolderPath())); |
148
|
|
|
|
149
|
|
|
$fs = new Filesystem(); |
150
|
|
|
$fs->remove($this->getFolderPath()); |
151
|
|
|
} |
152
|
|
|
|
153
|
|
|
/** |
154
|
|
|
* @return TmpRepositoryInterface |
155
|
|
|
*/ |
156
|
|
|
public function getTmpRepository(): TmpRepositoryInterface |
157
|
|
|
{ |
158
|
|
|
if (null === $this->tmpRepository) { |
159
|
|
|
$this->tmpRepository = Yii::createObject($this->tmpRepositoryClass, [$this->owner]); |
160
|
|
|
} |
161
|
|
|
|
162
|
|
|
return $this->tmpRepository; |
163
|
|
|
} |
164
|
|
|
|
165
|
|
|
/** |
166
|
|
|
* @return SourceRepositoryInterface |
167
|
|
|
*/ |
168
|
|
|
public function getSourceRepository(): SourceRepositoryInterface |
169
|
|
|
{ |
170
|
|
|
if (null === $this->sourceRepository) { |
171
|
|
|
$this->sourceRepository = Yii::createObject(array_merge($this->sourceRepositoryClass, [ |
172
|
|
|
'owner' => $this->owner, |
173
|
|
|
])); |
174
|
|
|
} |
175
|
|
|
|
176
|
|
|
return $this->sourceRepository; |
177
|
|
|
} |
178
|
|
|
|
179
|
|
|
/** |
180
|
|
|
* Get folder path |
181
|
|
|
* |
182
|
|
|
* @param string $field Field name |
183
|
|
|
* |
184
|
|
|
* @return string Return path to entity img with|out field name |
185
|
|
|
*/ |
186
|
|
|
public function getFolderPath(string $field = ''): string |
187
|
|
|
{ |
188
|
|
|
return $this->getSourceRepository()->getFolderPath($field); |
189
|
|
|
} |
190
|
|
|
|
191
|
|
|
/** |
192
|
|
|
* Get file path by "getFolderPath" |
193
|
|
|
* |
194
|
|
|
* @param string $field Field name |
195
|
|
|
* |
196
|
|
|
* @return string Return path to file |
197
|
|
|
*/ |
198
|
|
|
public function getFilePath(string $field): string |
199
|
|
|
{ |
200
|
|
|
return $this->getSourceRepository()->getFilePath($field); |
201
|
|
|
} |
202
|
|
|
|
203
|
|
|
/** |
204
|
|
|
* Get file src |
205
|
|
|
* |
206
|
|
|
* @param string $field |
207
|
|
|
* @param null $default |
208
|
|
|
* @param array $params |
209
|
|
|
* |
210
|
|
|
* @return string File src |
211
|
|
|
*/ |
212
|
|
|
public function getFileSrc(string $field, $default = '', array $params = []): string |
213
|
|
|
{ |
214
|
|
|
$repository = $this->getSourceRepository(); |
215
|
|
|
$result = $repository->getFileSrc($field, $default, $params); |
|
|
|
|
216
|
|
|
|
217
|
|
|
return $result; |
218
|
|
|
} |
219
|
|
|
|
220
|
|
|
/** |
221
|
|
|
* Clean data if was exception or return old data if record is not new |
222
|
|
|
* |
223
|
|
|
* @param $field |
224
|
|
|
*/ |
225
|
|
|
protected function cleanOnException($field) |
226
|
|
|
{ |
227
|
|
|
$this->removeTmpFile($field); |
228
|
|
|
if (false === $this->owner->isNewRecord) { |
229
|
|
|
$this->owner->attributes = $this->originValues; |
230
|
|
|
$this->owner->update(); |
231
|
|
|
} |
232
|
|
|
} |
233
|
|
|
|
234
|
|
|
/** |
235
|
|
|
* Prepare field for model |
236
|
|
|
*/ |
237
|
|
|
protected function prepareFields() |
238
|
|
|
{ |
239
|
|
|
foreach ($this->fields as $field) { |
240
|
|
|
if (false === isset($this->fieldsValues[$field])) { |
241
|
|
|
$file = $this->getTmpRepository()->getFile($field); |
242
|
|
|
if ($file) { |
243
|
|
|
$ext = pathinfo($file, PATHINFO_EXTENSION); |
244
|
|
|
$basename = basename($file); |
245
|
|
|
$maxLength = min(mb_strlen($basename) - mb_strlen($ext) - 1, $this->maxLength - mb_strlen($ext) - 1); |
246
|
|
|
$filename = sprintf('%s.%s', mb_substr(basename($file), 0, $maxLength), $ext); |
247
|
|
|
$this->owner->{$field} = $filename; |
248
|
|
|
} elseif (false === file_exists($this->getFilePath($field))) { |
249
|
|
|
//@todo Think about this. Because can not be record without files |
250
|
|
|
Yii::info('File does not exist: ' . $this->getFilePath($field), 'fileupload'); |
251
|
|
|
} |
252
|
|
|
} |
253
|
|
|
$this->fieldsValues[$field] = $this->owner->{$field}; |
254
|
|
|
} |
255
|
|
|
} |
256
|
|
|
|
257
|
|
|
/** |
258
|
|
|
* Events |
259
|
|
|
* |
260
|
|
|
* @return array |
261
|
|
|
*/ |
262
|
|
|
public function events(): array |
263
|
|
|
{ |
264
|
|
|
return [ |
265
|
|
|
ActiveRecord::EVENT_AFTER_FIND => 'afterFind', |
266
|
|
|
ActiveRecord::EVENT_BEFORE_VALIDATE => 'beforeValidate', |
267
|
|
|
ActiveRecord::EVENT_BEFORE_INSERT => 'beforeSave', |
268
|
|
|
ActiveRecord::EVENT_BEFORE_UPDATE => 'beforeSave', |
269
|
|
|
ActiveRecord::EVENT_AFTER_INSERT => 'afterSave', |
270
|
|
|
ActiveRecord::EVENT_AFTER_UPDATE => 'afterSave', |
271
|
|
|
ActiveRecord::EVENT_AFTER_DELETE => 'afterDelete', |
272
|
|
|
]; |
273
|
|
|
} |
274
|
|
|
|
275
|
|
|
/** |
276
|
|
|
* Save origin attributes to temp data |
277
|
|
|
* |
278
|
|
|
* @param Event $event event parameter |
279
|
|
|
*/ |
280
|
|
|
public function afterFind($event) |
|
|
|
|
281
|
|
|
{ |
282
|
|
|
foreach ($this->fields as $field) { |
283
|
|
|
if ($this->owner->hasAttribute($field)) { |
284
|
|
|
$this->originValues[$field] = $this->owner->{$field}; |
285
|
|
|
} |
286
|
|
|
} |
287
|
|
|
} |
288
|
|
|
|
289
|
|
|
/** |
290
|
|
|
* Prepare fields before validate |
291
|
|
|
* |
292
|
|
|
* @param Event $event parameter |
293
|
|
|
* |
294
|
|
|
* @author Orlov Alexey <[email protected]> |
295
|
|
|
*/ |
296
|
|
|
public function beforeValidate($event) |
|
|
|
|
297
|
|
|
{ |
298
|
|
|
$this->prepareFields(); |
299
|
|
|
} |
300
|
|
|
|
301
|
|
|
/** |
302
|
|
|
* If model save with flag false, call method for prepare field |
303
|
|
|
* |
304
|
|
|
* @param Event $event parameter |
305
|
|
|
*/ |
306
|
|
|
public function beforeSave($event) |
|
|
|
|
307
|
|
|
{ |
308
|
|
|
$this->prepareFields(); |
309
|
|
|
} |
310
|
|
|
|
311
|
|
|
/** |
312
|
|
|
* After save, file move in folder for model and delete temp file |
313
|
|
|
* |
314
|
|
|
* @param Event $event event parameter |
315
|
|
|
* |
316
|
|
|
* @throws Exception |
317
|
|
|
*/ |
318
|
|
|
public function afterSave($event) |
|
|
|
|
319
|
|
|
{ |
320
|
|
|
foreach ($this->fields as $field) { |
321
|
|
|
if ($tmpPath = $this->getTmpFile($field)) { |
322
|
|
|
if (false === is_dir($this->getFolderPath($field))) { |
323
|
|
|
if (false === FileHelper::createDirectory($this->getFolderPath($field), $this->mode, true)) { |
324
|
|
|
$this->cleanOnException($field); |
325
|
|
|
throw new FileException('Can\'t create directory!', 'Can\'t create directory: ' . $this->getFilePath($field), 500); |
326
|
|
|
} |
327
|
|
|
} |
328
|
|
|
$this->owner->{$field} = $this->fieldsValues[$field]; |
329
|
|
|
if (!@copy($tmpPath, $this->getFilePath($field))) { |
330
|
|
|
$this->cleanOnException($field); |
331
|
|
|
throw new FileException('File can\'t copy from!', 'File can\'t copy from ' . $tmpPath . ' to dest: ' . $this->getFilePath($field), 500); |
332
|
|
|
} |
333
|
|
|
$this->removeTmpFile($field); |
334
|
|
|
} |
335
|
|
|
$this->removeTmpFile($field); |
336
|
|
|
} |
337
|
|
|
} |
338
|
|
|
|
339
|
|
|
/** |
340
|
|
|
* Delete files from the server before removing data from the database. |
341
|
|
|
* |
342
|
|
|
* @param Event $event event parameter |
343
|
|
|
*/ |
344
|
|
|
public function afterDelete($event) |
|
|
|
|
345
|
|
|
{ |
346
|
|
|
$this->removeFiles(); |
347
|
|
|
} |
348
|
|
|
} |
349
|
|
|
|
This check looks at variables that have been passed in as parameters and are passed out again to other methods.
If the outgoing method call has stricter type requirements than the method itself, an issue is raised.
An additional type check may prevent trouble.