├── CHANGELOG.md
├── FileBehavior.php
├── ImageFileBehavior.php
├── LICENSE.md
├── README.md
├── TransformFileBehavior.php
└── composer.json
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | Yii 2 ActiveRecord File Attachment extension Change Log
2 | =======================================================
3 |
4 | 1.0.3, June 22, 2017
5 | --------------------
6 |
7 | - Enh #6: Added `FileBehavior::openFile()` method as a shortcut to `yii2tech\filestorage\BucketInterface::openFile()` (klimov-paul)
8 | - Enh #9: `FileBehavior::$subDirTemplate` now accepts the PHP callback, which should return its actual value (nexen2, klimov-paul)
9 |
10 |
11 | 1.0.2, October 7, 2016
12 | ----------------------
13 |
14 | - Bug #4: Fixed `TransformFileBehavior::getFileUrl()` triggers `E_NOTICE` in case `defaultFileUrl` is an empty array (klimov-paul)
15 | - Enh #3: Added support for transformed file extension variation via `TransformFileBehavior::transformationFileExtensions` (klimov-paul)
16 | - Enh #5: Added `TransformFileBehavior::regenerateFileTransformations()` method, allowing regeneration of the file transformations (klimov-paul)
17 |
18 |
19 | 1.0.1, February 14, 2016
20 | ------------------------
21 |
22 | - Bug #1: Fixed required version of "yii2tech/file-storage" preventing stable release composer install (klimov-paul)
23 |
24 |
25 | 1.0.0, February 10, 2016
26 | ------------------------
27 |
28 | - Initial release.
29 |
--------------------------------------------------------------------------------
/FileBehavior.php:
--------------------------------------------------------------------------------
1 |
42 | * @since 1.0
43 | */
44 | class FileBehavior extends Behavior
45 | {
46 | /**
47 | * @var string name of virtual model's attribute, which will be used
48 | * to fetch file uploaded from the web form.
49 | * Use value of this attribute to create web form file input field.
50 | */
51 | public $fileAttribute = 'file';
52 | /**
53 | * @var string name of the file storage application component.
54 | */
55 | public $fileStorage = 'fileStorage';
56 | /**
57 | * @var string|BucketInterface name of the file storage bucket, which stores the related files or
58 | * bucket instance itself.
59 | * If empty, bucket name will be generated automatically using owner class name and [[fileAttribute]].
60 | */
61 | public $fileStorageBucket;
62 | /**
63 | * @var string|callable template of all sub directories, which will store a particular
64 | * model instance's files. Value of this parameter will be parsed per each model instance.
65 | * You can use model attribute names to create sub directories, for example place all transformed
66 | * files in the subfolder with the name of model id. To use a dynamic value of attribute
67 | * place attribute name in curly brackets, for example: {id}.
68 | *
69 | * Since 1.0.3 template can be set as a callback returning actual string template:
70 | *
71 | * ```php
72 | * function (BaseActiveRecord $model) {
73 | * // return string, the actual path or mix it with placeholders
74 | * }
75 | * ```
76 | *
77 | * You may also specify special placeholders:
78 | *
79 | * - {pk} - resolved as primary key value of the owner model,
80 | * - {__model__} - resolved as class name of the owner model, replacing namespace separator (`\`) with underscore (`_`),
81 | * - {__file__} - resolved as value of [[fileAttribute]].
82 | *
83 | * You may place symbols "^" before any placeholder name, such placeholder will be resolved as single
84 | * symbol of the normal value. Number of symbol determined by count of "^".
85 | * For example: if model id equal to 54321, placeholder {^id} will be resolved as "5", {^^id} - as "4" and so on.
86 | * Example value:
87 | * '{__model__}/{__file__}/{groupId}/{^pk}/{pk}'
88 | */
89 | public $subDirTemplate = '{^^pk}/{^pk}';
90 | /**
91 | * @var string name of model's attribute, which will be used to store file extension.
92 | * Corresponding model's attribute should be a string type.
93 | */
94 | public $fileExtensionAttribute = 'fileExtension';
95 | /**
96 | * @var string name of model's attribute, which will be used to store file version number.
97 | * Corresponding model's attribute should be a string or integer type.
98 | */
99 | public $fileVersionAttribute = 'fileVersion';
100 | /**
101 | * @var int index of the HTML input file field in case of tabular input (input name has format "ModelName[$i][file]").
102 | * Note: after owner is saved this property will be reset.
103 | */
104 | public $fileTabularInputIndex;
105 | /**
106 | * @var string URL which is used to set up web links, which will be returned, if requested file does not exists.
107 | * For example: 'http://www.myproject.com/materials/default/image.jpg'
108 | */
109 | public $defaultFileUrl;
110 | /**
111 | * @var bool indicates if behavior will attempt to fetch uploaded file automatically from the HTTP request.
112 | */
113 | public $autoFetchUploadedFile = true;
114 |
115 | /**
116 | * @var UploadedFile instance of [[UploadedFile]], allows to save file,
117 | * passed through the web form.
118 | */
119 | private $_uploadedFile;
120 |
121 | // Set / Get:
122 |
123 | /**
124 | * @param UploadedFile|string|null $uploadedFile related uploaded file
125 | */
126 | public function setUploadedFile($uploadedFile)
127 | {
128 | $this->_uploadedFile = $uploadedFile;
129 | }
130 |
131 | /**
132 | * @return UploadedFile|null related uploaded file
133 | */
134 | public function getUploadedFile()
135 | {
136 | if (!is_object($this->_uploadedFile)) {
137 | $this->_uploadedFile = $this->ensureUploadedFile($this->_uploadedFile);
138 | }
139 | return $this->_uploadedFile;
140 | }
141 |
142 | /**
143 | * Returns the file storage bucket for the files by name given with [[fileStorageBucket]].
144 | * If no bucket exists attempts to create it.
145 | * @return BucketInterface file storage bucket instance.
146 | */
147 | public function ensureFileStorageBucket()
148 | {
149 | if (!is_object($this->fileStorageBucket)) {
150 | /* @var StorageInterface $fileStorage */
151 | $fileStorage = Instance::ensure($this->fileStorage, 'yii2tech\filestorage\StorageInterface');
152 |
153 | if ($this->fileStorageBucket === null) {
154 | $bucketName = $this->defaultFileStorageBucketName();
155 | } else {
156 | $bucketName = $this->fileStorageBucket;
157 | }
158 | if (!$fileStorage->hasBucket($bucketName)) {
159 | $fileStorage->addBucket($bucketName);
160 | }
161 | $this->fileStorageBucket = $fileStorage->getBucket($bucketName);
162 | }
163 | return $this->fileStorageBucket;
164 | }
165 |
166 | /**
167 | * Composes default [[fileStorageBucket]] name, using owner class name and [[fileAttribute]].
168 | * @return string bucket name.
169 | */
170 | protected function defaultFileStorageBucketName()
171 | {
172 | return Inflector::camel2id(StringHelper::basename(get_class($this->owner)), '-');
173 | }
174 |
175 | // SubDir Template:
176 |
177 | /**
178 | * Gets file storage sub dirs path, resolving [[subDirTemplate]].
179 | * @return string actual sub directory string.
180 | */
181 | public function getActualSubDir()
182 | {
183 | if (!is_scalar($this->subDirTemplate) && is_callable($this->subDirTemplate)) {
184 | $subDirTemplate = call_user_func($this->subDirTemplate, $this->owner);
185 | } else {
186 | $subDirTemplate = $this->subDirTemplate;
187 | }
188 | if (empty($subDirTemplate)) {
189 | return $subDirTemplate;
190 | }
191 | $result = preg_replace_callback('/{(\^*(\w+))}/', [$this, 'getSubDirPlaceholderValue'], $subDirTemplate);
192 | return $result;
193 | }
194 |
195 | /**
196 | * Internal callback function for [[getActualSubDir()]].
197 | * @param array $matches - set of regular expression matches.
198 | * @return string replacement for the match.
199 | */
200 | protected function getSubDirPlaceholderValue($matches)
201 | {
202 | $placeholderName = $matches[1];
203 | $placeholderPartSymbolPosition = strspn($placeholderName, '^') - 1;
204 | if ($placeholderPartSymbolPosition >= 0) {
205 | $placeholderName = $matches[2];
206 | }
207 |
208 | switch ($placeholderName) {
209 | case 'pk': {
210 | $placeholderValue = $this->getPrimaryKeyStringValue();
211 | break;
212 | }
213 | case '__model__': {
214 | $placeholderValue = str_replace('\\', '_', get_class($this->owner));
215 | break;
216 | }
217 | case '__file__': {
218 | $placeholderValue = $this->fileAttribute;
219 | break;
220 | }
221 | default: {
222 | try {
223 | $placeholderValue = $this->owner->{$placeholderName};
224 | } catch (UnknownPropertyException $exception) {
225 | $placeholderValue = $placeholderName;
226 | }
227 | }
228 | }
229 |
230 | if ($placeholderPartSymbolPosition >= 0) {
231 | if ($placeholderPartSymbolPosition < strlen($placeholderValue)) {
232 | $placeholderValue = substr($placeholderValue, $placeholderPartSymbolPosition, 1);
233 | } else {
234 | $placeholderValue = '0';
235 | }
236 | }
237 |
238 | return $placeholderValue;
239 | }
240 |
241 | // Service:
242 |
243 | /**
244 | * Creates string representation of owner model primary key value,
245 | * handles case when primary key is complex and consist of several fields.
246 | * @return string representation of owner model primary key value.
247 | */
248 | protected function getPrimaryKeyStringValue()
249 | {
250 | $owner = $this->owner;
251 | $primaryKey = $owner->getPrimaryKey();
252 | if (is_array($primaryKey)) {
253 | return implode('_', $primaryKey);
254 | }
255 | return $primaryKey;
256 | }
257 |
258 | /**
259 | * Creates base part of the file name.
260 | * This value will be append with the version and extension for the particular file.
261 | * @return string file name's base part.
262 | */
263 | protected function getFileBaseName()
264 | {
265 | return $this->getPrimaryKeyStringValue();
266 | }
267 |
268 | /**
269 | * Returns current version value of the model's file.
270 | * @return int current version of model's file.
271 | */
272 | public function getCurrentFileVersion()
273 | {
274 | $owner = $this->owner;
275 | return $owner->getAttribute($this->fileVersionAttribute);
276 | }
277 |
278 | /**
279 | * Returns next version value of the model's file.
280 | * @return int next version of model's file.
281 | */
282 | public function getNextFileVersion()
283 | {
284 | return $this->getCurrentFileVersion() + 1;
285 | }
286 |
287 | /**
288 | * Creates file itself name (without path) including version and extension.
289 | * @param int $fileVersion file version number.
290 | * @param string $fileExtension file extension.
291 | * @return string file self name.
292 | */
293 | public function getFileSelfName($fileVersion = null, $fileExtension = null)
294 | {
295 | $owner = $this->owner;
296 | if ($fileVersion === null) {
297 | $fileVersion = $this->getCurrentFileVersion();
298 | }
299 | if ($fileExtension === null) {
300 | $fileExtension = $owner->getAttribute($this->fileExtensionAttribute);
301 | }
302 | return $this->getFileBaseName() . '_' . $fileVersion . '.' . $fileExtension;
303 | }
304 |
305 | /**
306 | * Creates the file name in the file storage.
307 | * This name contains the sub directory, resolved by [[subDirTemplate]].
308 | * @param int $fileVersion file version number.
309 | * @param string $fileExtension file extension.
310 | * @return string file full name.
311 | */
312 | public function getFileFullName($fileVersion = null, $fileExtension = null)
313 | {
314 | $fileName = $this->getFileSelfName($fileVersion, $fileExtension);
315 | $subDir = $this->getActualSubDir();
316 | if (!empty($subDir)) {
317 | $fileName = $subDir . DIRECTORY_SEPARATOR . $fileName;
318 | }
319 | return $fileName;
320 | }
321 |
322 | // Main File Operations:
323 |
324 | /**
325 | * Associate new file with the owner model.
326 | * This method will determine new file version and extension, and will update the owner
327 | * model correspondingly.
328 | * @param string|UploadedFile $sourceFileNameOrUploadedFile file system path to source file or [[UploadedFile]] instance.
329 | * @param bool $deleteSourceFile determines would the source file be deleted in the process or not,
330 | * if null given file will be deleted if it was uploaded via POST.
331 | * @return bool save success.
332 | */
333 | public function saveFile($sourceFileNameOrUploadedFile, $deleteSourceFile = null)
334 | {
335 | $this->deleteFile();
336 |
337 | $fileVersion = $this->getNextFileVersion();
338 |
339 | if (is_object($sourceFileNameOrUploadedFile)) {
340 | $sourceFileName = $sourceFileNameOrUploadedFile->tempName;
341 | $fileExtension = $sourceFileNameOrUploadedFile->getExtension();
342 | } else {
343 | $sourceFileName = $sourceFileNameOrUploadedFile;
344 | $fileExtension = strtolower(pathinfo($sourceFileName, PATHINFO_EXTENSION));
345 | }
346 |
347 | $result = $this->newFile($sourceFileName, $fileVersion, $fileExtension);
348 |
349 | if ($result) {
350 | if ($deleteSourceFile === null) {
351 | $deleteSourceFile = is_uploaded_file($sourceFileName);
352 | }
353 | if ($deleteSourceFile) {
354 | unlink($sourceFileName);
355 | }
356 |
357 | $owner = $this->owner;
358 |
359 | $attributes = [
360 | $this->fileVersionAttribute => $fileVersion,
361 | $this->fileExtensionAttribute => $fileExtension
362 | ];
363 | $owner->updateAttributes($attributes);
364 | }
365 |
366 | return $result;
367 | }
368 |
369 | /**
370 | * Creates the file for the model from the source file.
371 | * File version and extension are passed to this method.
372 | * @param string $sourceFileName - source full file name.
373 | * @param int $fileVersion - file version number.
374 | * @param string $fileExtension - file extension.
375 | * @return bool success.
376 | */
377 | protected function newFile($sourceFileName, $fileVersion, $fileExtension)
378 | {
379 | $fileFullName = $this->getFileFullName($fileVersion, $fileExtension);
380 | $fileStorageBucket = $this->ensureFileStorageBucket();
381 | return $fileStorageBucket->copyFileIn($sourceFileName, $fileFullName);
382 | }
383 |
384 | /**
385 | * Removes file associated with the owner model.
386 | * @return bool success.
387 | */
388 | public function deleteFile()
389 | {
390 | $fileStorageBucket = $this->ensureFileStorageBucket();
391 | $fileName = $this->getFileFullName();
392 | if ($fileStorageBucket->fileExists($fileName)) {
393 | return $fileStorageBucket->deleteFile($fileName);
394 | }
395 | return true;
396 | }
397 |
398 | /**
399 | * Finds the uploaded through the web file, creating [[UploadedFile]] instance.
400 | * If parameter $fullFileName is passed, creates a mock up instance of [[UploadedFile]] from the local file,
401 | * passed with this parameter.
402 | * @param UploadedFile|string|null $uploadedFile - source full file name for the [[UploadedFile]] mock up.
403 | * @return UploadedFile|null uploaded file.
404 | */
405 | protected function ensureUploadedFile($uploadedFile = null)
406 | {
407 | if ($uploadedFile instanceof UploadedFile) {
408 | return $uploadedFile;
409 | }
410 |
411 | if (!empty($uploadedFile)) {
412 | return new UploadedFile([
413 | 'name' => basename($uploadedFile),
414 | 'tempName' => $uploadedFile,
415 | 'type' => FileHelper::getMimeType($uploadedFile),
416 | 'size' => filesize($uploadedFile),
417 | 'error' => UPLOAD_ERR_OK
418 | ]);
419 | }
420 |
421 | if ($this->autoFetchUploadedFile) {
422 | $owner = $this->owner;
423 | $fileAttributeName = $this->fileAttribute;
424 | $tabularInputIndex = $this->fileTabularInputIndex;
425 | if ($tabularInputIndex !== null) {
426 | $fileAttributeName = "[{$tabularInputIndex}]{$fileAttributeName}";
427 | }
428 | $uploadedFile = UploadedFile::getInstance($owner, $fileAttributeName);
429 | if (is_object($uploadedFile)) {
430 | if (!$uploadedFile->getHasError() && !file_exists($uploadedFile->tempName)) {
431 | // uploaded file has been already processed:
432 | return null;
433 | } else {
434 | return $uploadedFile;
435 | }
436 | }
437 | }
438 |
439 | return null;
440 | }
441 |
442 | // File Interface Function Shortcuts:
443 |
444 | /**
445 | * Checks if file related to the model exists.
446 | * @return bool file exists.
447 | */
448 | public function fileExists()
449 | {
450 | $fileStorageBucket = $this->ensureFileStorageBucket();
451 | return $fileStorageBucket->fileExists($this->getFileFullName());
452 | }
453 |
454 | /**
455 | * Returns the content of the model related file.
456 | * @return string file content.
457 | */
458 | public function getFileContent()
459 | {
460 | $fileStorageBucket = $this->ensureFileStorageBucket();
461 | return $fileStorageBucket->getFileContent($this->getFileFullName());
462 | }
463 |
464 | /**
465 | * Returns full web link to the model related file.
466 | * @return string web link to file.
467 | */
468 | public function getFileUrl()
469 | {
470 | $fileStorageBucket = $this->ensureFileStorageBucket();
471 | $fileFullName = $this->getFileFullName();
472 | if ($this->defaultFileUrl !== null) {
473 | if (!$fileStorageBucket->fileExists($fileFullName)) {
474 | return $this->defaultFileUrl;
475 | }
476 | }
477 | return $fileStorageBucket->getFileUrl($fileFullName);
478 | }
479 |
480 | /**
481 | * Opens a file as stream resource, e.g. like `fopen()` function.
482 | * @param string $mode - the type of access you require to the stream, e.g. `r`, `w`, `a` and so on.
483 | * You should prefer usage of simple modes like `r` and `w`, avoiding complex ones like `w+`, as they
484 | * may not supported by some storages.
485 | * @return resource|false file pointer resource on success, or `false` on error.
486 | * @since 1.0.3
487 | */
488 | public function openFile($mode)
489 | {
490 | $fileStorageBucket = $this->ensureFileStorageBucket();
491 | return $fileStorageBucket->openFile($this->getFileFullName(), $mode);
492 | }
493 |
494 | // Property Access Extension:
495 |
496 | /**
497 | * PHP getter magic method.
498 | * This method is overridden so that variation attributes can be accessed like properties.
499 | *
500 | * @param string $name property name
501 | * @throws UnknownPropertyException if the property is not defined
502 | * @return mixed property value
503 | */
504 | public function __get($name)
505 | {
506 | try {
507 | return parent::__get($name);
508 | } catch (UnknownPropertyException $exception) {
509 | if ($this->owner !== null) {
510 | if ($name === $this->fileAttribute) {
511 | return $this->getUploadedFile();
512 | }
513 | }
514 | throw $exception;
515 | }
516 | }
517 |
518 | /**
519 | * PHP setter magic method.
520 | * This method is overridden so that variation attributes can be accessed like properties.
521 | * @param string $name property name
522 | * @param mixed $value property value
523 | * @throws UnknownPropertyException if the property is not defined
524 | */
525 | public function __set($name, $value)
526 | {
527 | try {
528 | parent::__set($name, $value);
529 | } catch (UnknownPropertyException $exception) {
530 | if ($this->owner !== null) {
531 | if ($name === $this->fileAttribute) {
532 | $this->setUploadedFile($value);
533 | return;
534 | }
535 | }
536 | throw $exception;
537 | }
538 | }
539 |
540 | /**
541 | * @inheritdoc
542 | */
543 | public function canGetProperty($name, $checkVars = true)
544 | {
545 | if (parent::canGetProperty($name, $checkVars)) {
546 | return true;
547 | }
548 | if ($this->owner === null) {
549 | return false;
550 | }
551 | return ($name === $this->fileAttribute);
552 | }
553 |
554 | /**
555 | * @inheritdoc
556 | */
557 | public function canSetProperty($name, $checkVars = true)
558 | {
559 | if (parent::canSetProperty($name, $checkVars)) {
560 | return true;
561 | }
562 | if ($this->owner === null) {
563 | return false;
564 | }
565 | return ($name === $this->fileAttribute);
566 | }
567 |
568 | // Events:
569 |
570 | /**
571 | * Declares events and the corresponding event handler methods.
572 | * @return array events (array keys) and the corresponding event handler methods (array values).
573 | */
574 | public function events()
575 | {
576 | return [
577 | BaseActiveRecord::EVENT_AFTER_INSERT => 'afterSave',
578 | BaseActiveRecord::EVENT_AFTER_UPDATE => 'afterSave',
579 | BaseActiveRecord::EVENT_AFTER_DELETE => 'afterDelete',
580 | ];
581 | }
582 |
583 | /**
584 | * This event raises after owner saved.
585 | * It saves uploaded file if it exists.
586 | * @param \yii\base\Event $event event instance.
587 | */
588 | public function afterSave($event)
589 | {
590 | $uploadedFile = $this->getUploadedFile();
591 | if (is_object($uploadedFile) && !$uploadedFile->getHasError()) {
592 | $this->saveFile($uploadedFile);
593 | }
594 | $this->setUploadedFile(null);
595 | }
596 |
597 | /**
598 | * This event raises before owner deleted.
599 | * It deletes related file.
600 | * @param \yii\base\Event $event event instance.
601 | */
602 | public function afterDelete($event)
603 | {
604 | $this->deleteFile();
605 | }
606 | }
--------------------------------------------------------------------------------
/ImageFileBehavior.php:
--------------------------------------------------------------------------------
1 | [800, 600],
25 | * 'thumbnail' => [200, 150]
26 | * ];
27 | * ```
28 | *
29 | * In order save original file without any transformations, set string value with native key.
30 | * For example:
31 | *
32 | * ```php
33 | * [
34 | * 'origin',
35 | * 'main' => [800, 600],
36 | * 'thumbnail' => [200, 150]
37 | * ];
38 | * ```
39 | *
40 | * Note: you can always use [[saveFile()]] method to attach any file (not just uploaded one) to the model.
41 | *
42 | * Attention: this extension requires the extension "yiisoft/yii2-imagine" to be attached to the application!
43 | *
44 | * @see TransformFileBehavior
45 | *
46 | * @author Paul Klimov
47 | * @since 1.0
48 | */
49 | class ImageFileBehavior extends TransformFileBehavior
50 | {
51 | /**
52 | * @inheritdoc
53 | */
54 | protected function transformFile($sourceFileName, $destinationFileName, $transformationSettings)
55 | {
56 | if ($this->transformCallback === null) {
57 | return $this->transformImageFileResize($sourceFileName, $destinationFileName, $transformationSettings);
58 | }
59 | return parent::transformFile($sourceFileName, $destinationFileName, $transformationSettings);
60 | }
61 |
62 | /**
63 | * Resizes source file to destination file according to the transformation settings, using [[Image::thumbnail()]].
64 | * @param string $sourceFileName is the full source file system name.
65 | * @param string $destinationFileName is the full destination file system name.
66 | * @param array $transformSettings is the transform settings data, it should be the pair: 'imageWidth' and 'imageHeight',
67 | * For example: `[800, 600]`
68 | * @throws InvalidConfigException on invalid transform settings.
69 | * @return bool success.
70 | */
71 | protected function transformImageFileResize($sourceFileName, $destinationFileName, $transformSettings)
72 | {
73 | if (!is_array($transformSettings)) {
74 | throw new InvalidConfigException('Wrong transform settings are passed to "' . get_class($this) . '::' . __FUNCTION__ . '"');
75 | }
76 | list($width, $height) = array_values($transformSettings);
77 | Image::thumbnail($sourceFileName, $width, $height)->save($destinationFileName);
78 | return true;
79 | }
80 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The Yii framework is free software. It is released under the terms of
2 | the following BSD License.
3 |
4 | Copyright © 2015 by Yii2tech (https://github.com/yii2tech)
5 | All rights reserved.
6 |
7 | Redistribution and use in source and binary forms, with or without
8 | modification, are permitted provided that the following conditions
9 | are met:
10 |
11 | * Redistributions of source code must retain the above copyright
12 | notice, this list of conditions and the following disclaimer.
13 | * Redistributions in binary form must reproduce the above copyright
14 | notice, this list of conditions and the following disclaimer in
15 | the documentation and/or other materials provided with the
16 | distribution.
17 | * Neither the name of Yii2tech nor the names of its
18 | contributors may be used to endorse or promote products derived
19 | from this software without specific prior written permission.
20 |
21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
24 | FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
25 | COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
26 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
27 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
28 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
29 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
30 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
31 | ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
32 | POSSIBILITY OF SUCH DAMAGE.
33 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
ActiveRecord File Attachment Extension for Yii2
6 |
7 |
8 |
9 | This extension provides support for ActiveRecord file attachment.
10 |
11 | For license information check the [LICENSE](LICENSE.md)-file.
12 |
13 | [](https://packagist.org/packages/yii2tech/ar-file)
14 | [](https://packagist.org/packages/yii2tech/ar-file)
15 | [](https://travis-ci.org/yii2tech/ar-file)
16 |
17 |
18 | Installation
19 | ------------
20 |
21 | The preferred way to install this extension is through [composer](http://getcomposer.org/download/).
22 |
23 | Either run
24 |
25 | ```
26 | php composer.phar require --prefer-dist yii2tech/ar-file
27 | ```
28 |
29 | or add
30 |
31 | ```json
32 | "yii2tech/ar-file": "*"
33 | ```
34 |
35 | to the require section of your composer.json.
36 |
37 | If you wish to use [[yii2tech\ar\file\ImageFileBehavior]], you will also need to install [yiisoft/yii2-imagine](https://github.com/yiisoft/yii2-imagine),
38 | which is not required by default. In order to do so either run
39 |
40 | ```
41 | php composer.phar require --prefer-dist yiisoft/yii2-imagine
42 | ```
43 |
44 | or add
45 |
46 | ```json
47 | "yiisoft/yii2-imagine": "*"
48 | ```
49 |
50 | to the require section of your composer.json.
51 |
52 |
53 | Usage
54 | -----
55 |
56 | This extension provides support for ActiveRecord file attachment. Attached files are stored inside separated file storage,
57 | which does not connected with ActiveRecord database.
58 |
59 | This extension based on [yii2tech/file-storage](https://github.com/yii2tech/file-storage), and uses it as a
60 | file saving layer. Thus attached files can be stored at any file storage such as local file system, Amazon S3 and so on.
61 |
62 | First of all, you need to configure file storage, which will be used for attached files:
63 |
64 | ```php
65 | return [
66 | 'components' => [
67 | 'fileStorage' => [
68 | 'class' => 'yii2tech\filestorage\local\Storage',
69 | 'basePath' => '@webroot/files',
70 | 'baseUrl' => '@web/files',
71 | 'filePermission' => 0777,
72 | 'buckets' => [
73 | 'item' => [
74 | 'baseSubPath' => 'item',
75 | ],
76 | ]
77 | ],
78 | // ...
79 | ],
80 | // ...
81 | ];
82 | ```
83 |
84 | You should use [[\yii2tech\ar\file\FileBehavior]] behavior in order to allow your ActiveRecord file saving.
85 | This can be done in following way:
86 |
87 | ```php
88 | use yii2tech\ar\file\FileBehavior;
89 |
90 | class Item extends \yii\db\ActiveRecord
91 | {
92 | public function behaviors()
93 | {
94 | return [
95 | 'file' => [
96 | 'class' => FileBehavior::className(),
97 | 'fileStorageBucket' => 'item',
98 | 'fileExtensionAttribute' => 'fileExtension',
99 | 'fileVersionAttribute' => 'fileVersion',
100 | ],
101 | ];
102 | }
103 | // ...
104 | }
105 | ```
106 |
107 | Usage of this behavior requires extra columns being present at the owner entity (database table):
108 |
109 | - [[\yii2tech\ar\file\FileBehavior::fileExtensionAttribute]] - used to store file extension, allowing to determine file type
110 | - [[\yii2tech\ar\file\FileBehavior::fileVersionAttribute]] - used to track file version, allowing browser cache busting
111 |
112 | For example, DDL for the 'item' table may look like following:
113 |
114 | ```sql
115 | CREATE TABLE `Item`
116 | (
117 | `id` integer NOT NULL AUTO_INCREMENT,
118 | `name` varchar(64) NOT NULL,
119 | `description` text,
120 | `fileExtension` varchar(10),
121 | `fileVersion` integer,
122 | PRIMARY KEY (`id`)
123 | ) ENGINE InnoDB;
124 | ```
125 |
126 | Once behavior is attached to may use `saveFile()` method on your ActiveRecord instance:
127 |
128 | ```php
129 | $model = Item::findOne(1);
130 | $model->saveFile('/path/to/source/file.dat');
131 | ```
132 |
133 | This method will save source file inside file storage bucket, which has been specified inside behavior configuration,
134 | and update file extension and version attributes.
135 |
136 | You may delete existing file using `deleteFile()` method:
137 |
138 | ```php
139 | $model = Item::findOne(1);
140 | $model->deleteFile();
141 | ```
142 |
143 | > Note: attached file will be automatically removed on owner deletion (`delete()` method invocation).
144 |
145 | You may check existence of the file, get its content or URL:
146 |
147 | ```php
148 | $model = Item::findOne(1);
149 | if ($model->fileExists()) {
150 | echo $model->getFileUrl(); // outputs file URL
151 | echo $model->getFileContent(); // outputs file content
152 | } else {
153 | echo 'No file attached';
154 | }
155 | ```
156 |
157 | > Tip: you may setup [[\yii2tech\ar\file\FileBehavior::defaultFileUrl]] in order to make `getFileUrl()`
158 | returning some default image URL in case actual attached file is missing.
159 |
160 |
161 | ## Working with web forms
162 |
163 | Usually files for ActiveRecord are setup via web interface using file upload mechanism.
164 | [[\yii2tech\ar\file\FileBehavior]] provides a special virtual property for the owner, which name is determined
165 | by [[\yii2tech\ar\file\FileBehavior::fileAttribute]]. This property can be used to pass [[\yii\web\UploadedFile]]
166 | instance or local file name, which should be attached to the ActiveRecord. This property is processed on
167 | owner saving, and if set will trigger file saving. For example:
168 |
169 | ```php
170 | use yii\web\UploadedFile;
171 |
172 | $model = Item::findOne(1);
173 | $model->file = UploadedFile::getInstance($model, 'file');
174 | $model->save();
175 |
176 | var_dump($model->fileExists()); // outputs `true`
177 | ```
178 |
179 | > Attention: do NOT declare [[\yii2tech\ar\file\FileBehavior::fileAttribute]] attribute in the owner ActiveRecord class.
180 | Make sure it does not conflict with any existing owner field or virtual property.
181 |
182 | If [[\yii2tech\ar\file\FileBehavior::autoFetchUploadedFile]] is enabled, behavior will attempt to fetch
183 | uploaded file automatically before owner saving.
184 |
185 | You may setup a validation rules for the file virtual attribute inside your model, specifying restrictions
186 | for the attached file type, extension and so on:
187 |
188 | ```php
189 | class Item extends \yii\db\ActiveRecord
190 | {
191 | public function rules()
192 | {
193 | return [
194 | // ...
195 | ['file', 'file', 'mimeTypes' => ['image/jpeg', 'image/pjpeg', 'image/png', 'image/gif'], 'skipOnEmpty' => !$this->isNewRecord],
196 | ];
197 | }
198 | // ...
199 | }
200 | ```
201 |
202 | Inside view file you can use file virtual property for the form file input as it belongs to the owner model itself:
203 |
204 | ```php
205 |
211 | ['enctype' => 'multipart/form-data']]); ?>
212 |
213 | = $form->field($model, 'name'); ?>
214 | = $form->field($model, 'description'); ?>
215 |
216 | = $form->field($model, 'file')->fileInput(); ?>
217 |
218 |
219 | = Html::submitButton('Save', ['class' => 'btn btn-primary']) ?>
220 |
221 |
222 |
223 | ```
224 |
225 | Inside the controller you don't need any special code:
226 |
227 | ```php
228 | use yii\web\Controller;
229 |
230 | class ItemController extends Controller
231 | {
232 | public function actionCreate()
233 | {
234 | $model = new Item();
235 |
236 | if ($model->load(Yii::$app->request->post()) && $model->save()) {
237 | return $this->redirect(['view']);
238 | } else {
239 | return $this->render('create', [
240 | 'model' => $model,
241 | ]);
242 | }
243 | }
244 |
245 | // ...
246 | }
247 | ```
248 |
249 |
250 | ## File transformation
251 |
252 | Saving file "as it is" is not always enough for ActiveRecord attachment. Often files require some
253 | processing, like image resizing, for example.
254 |
255 | [[\yii2tech\ar\file\TransformFileBehavior]] is an enhanced version of the [[FileBehavior]] developed for
256 | the managing files, which require some processing (transformations).
257 | You should setup [[\yii2tech\ar\file\TransformFileBehavior::transformCallback]] to specify actual file processing
258 | algorithm, and [[\yii2tech\ar\file\TransformFileBehavior::fileTransformations]] providing the list of named
259 | processing and their specific settings. For example:
260 |
261 | ```php
262 | use yii2tech\ar\file\TransformFileBehavior;
263 | use yii\imagine\Image;
264 |
265 | class Item extends \yii\db\ActiveRecord
266 | {
267 | public function behaviors()
268 | {
269 | return [
270 | 'file' => [
271 | 'class' => TransformFileBehavior::className(),
272 | 'fileStorageBucket' => 'item',
273 | 'fileExtensionAttribute' => 'fileExtension',
274 | 'fileVersionAttribute' => 'fileVersion',
275 | 'transformCallback' => function ($sourceFileName, $destinationFileName, $options) {
276 | try {
277 | Image::thumbnail($sourceFileName, $options['width'], $options['height'])->save($destinationFileName);
278 | return true;
279 | } catch (\Exception $e) {
280 | return false;
281 | }
282 | },
283 | 'fileTransformations' => [
284 | 'origin', // no transformation
285 | 'main' => [
286 | 'width' => 400,
287 | 'height' => 400,
288 | ],
289 | 'thumbnail' => [
290 | 'width' => 100,
291 | 'height' => 100,
292 | ],
293 | ],
294 | ],
295 | ];
296 | }
297 | // ...
298 | }
299 | ```
300 |
301 | In case of usage [[\yii2tech\ar\file\TransformFileBehavior]] methods `fileExists()`, `getFileContent()` and `getFileUrl()`
302 | accepts first parameter as a transformation name, for which result should be returned:
303 |
304 | ```php
305 | $model = Item::findOne(1);
306 | echo $model->getFileUrl('origin'); // outputs URL for the full-sized image
307 | echo $model->getFileUrl('main'); // outputs URL for the medium-sized image
308 | echo $model->getFileUrl('thumbnail'); // outputs URL for the thumbnail image
309 | ```
310 |
311 | Some file transformations may require changing the file extension. For example: you may want to create a preview for the
312 | *.psd file in *.jpg format. You may specify file extension per each transformation using [[\yii2tech\ar\file\TransformFileBehavior::transformationFileExtensions]].
313 | For example:
314 |
315 | ```php
316 | use yii2tech\ar\file\TransformFileBehavior;
317 | use yii\imagine\Image;
318 |
319 | class Item extends \yii\db\ActiveRecord
320 | {
321 | public function behaviors()
322 | {
323 | return [
324 | 'file' => [
325 | 'class' => TransformFileBehavior::className(),
326 | 'fileTransformations' => [
327 | 'origin', // no transformation
328 | 'preview' => [
329 | // ...
330 | ],
331 | 'web' => [
332 | // ...
333 | ],
334 | ],
335 | 'transformationFileExtensions' => [
336 | 'preview' => 'jpg',
337 | 'web' => function ($fileExtension) {
338 | return in_array($fileExtension, ['jpg', 'jpeg', 'png', 'gif']) ? $fileExtension : 'jpg';
339 | },
340 | ],
341 | // ...
342 | ],
343 | ];
344 | }
345 | // ...
346 | }
347 | ```
348 |
349 | You may face the issue, when settings for some file transformations change or new transformation added, as your project
350 | evolves, making existing saved files outdated. In this case you can use [[\yii2tech\ar\file\TransformFileBehavior::regenerateFileTransformations()]]
351 | method to regenerate transformation files with new settings using some existing transformation as source.
352 | For example:
353 |
354 | ```php
355 | $model = Item::findOne(1);
356 | $model->regenerateFileTransformations('origin'); // regenerate transformations using 'origin' as a source
357 | ```
358 |
359 |
360 | ## Image file transformation
361 |
362 | The most common file transformation use case is an image resizing. Thus a special behavior
363 | [[\yii2tech\ar\file\ImageFileBehavior]] is provided. This behavior provides image resize transformation
364 | via [yiisoft/yii2-imagine](https://github.com/yiisoft/yii2-imagine) extension.
365 | Configuration example:
366 |
367 | ```php
368 | use yii2tech\ar\file\ImageFileBehavior;
369 |
370 | class Item extends \yii\db\ActiveRecord
371 | {
372 | public function behaviors()
373 | {
374 | return [
375 | 'file' => [
376 | 'class' => ImageFileBehavior::className(),
377 | 'fileStorageBucket' => 'item',
378 | 'fileExtensionAttribute' => 'fileExtension',
379 | 'fileVersionAttribute' => 'fileVersion',
380 | 'fileTransformations' => [
381 | 'origin', // no resize
382 | 'main' => [800, 600], // width = 800px, height = 600px
383 | 'thumbnail' => [100, 80], // width = 100px, height = 80px
384 | ],
385 | ],
386 | ];
387 | }
388 | // ...
389 | }
390 | ```
391 |
392 | > Note: this package does not include "yiisoft/yii2-imagine", you should install it yourself.
393 |
--------------------------------------------------------------------------------
/TransformFileBehavior.php:
--------------------------------------------------------------------------------
1 |
29 | * @since 1.0
30 | */
31 | class TransformFileBehavior extends FileBehavior
32 | {
33 | /**
34 | * @inheritdoc
35 | */
36 | public $subDirTemplate = '{^^pk}/{^pk}/{pk}';
37 | /**
38 | * @var array determines all possible file transformations.
39 | * The key of array element is the name of transformation and will be used to create file name.
40 | * The value is an array of parameters for transformation. Its value depends on which [[transformCallback]] you are using.
41 | * If you wish to save original file without transformation, specify a key without value.
42 | * For example:
43 | *
44 | * ```php
45 | * [
46 | * 'origin',
47 | * 'main' => [...],
48 | * 'light' => [...],
49 | * ];
50 | * ```
51 | */
52 | public $fileTransformations = [];
53 | /**
54 | * @var callable a PHP callback, which will be called while file transforming. The signature of the callback should
55 | * be following:
56 | *
57 | * ```php
58 | * function(string $sourceFileName, string $destinationFileName, mixed $transformationSettings) {
59 | * //return bool;
60 | * }
61 | * ```
62 | *
63 | * Callback should return bool, which indicates whether transformation was successful or not.
64 | */
65 | public $transformCallback;
66 | /**
67 | * @var array|callable|null file extension specification for the file transformation results.
68 | * This value can be an array in format: [transformationName => fileExtension], for example:
69 | *
70 | * ```php
71 | * [
72 | * 'preview' => 'jpg',
73 | * 'archive' => 'zip',
74 | * ]
75 | * ```
76 | *
77 | * Each extension specification can be a PHP callback, which accepts original extension and should return actual.
78 | * For example:
79 | *
80 | * ```php
81 | * [
82 | * 'image' => function ($fileExtension) {
83 | * return in_array($fileExtension, ['jpg', 'jpeg', 'png', 'gif']) ? $fileExtension : 'jpg';
84 | * },
85 | * ]
86 | * ```
87 | *
88 | * You may specify this field as a single PHP callback of following signature:
89 | *
90 | * ```php
91 | * function (string $fileExtension, string $transformationName) {
92 | * //return string actual extension;
93 | * }
94 | * ```
95 | *
96 | * @since 1.0.2
97 | */
98 | public $transformationFileExtensions;
99 | /**
100 | * @var string path, which should be used for storing temporary files during transformation.
101 | * If not set, default one will be composed inside '@runtime' directory.
102 | * Path aliases like '@webroot' and '@runtime' can be used here.
103 | */
104 | public $transformationTempFilePath;
105 | /**
106 | * @var string|array URL(s), which is used to set up web links, which will be returned if requested file does not exists.
107 | * If may specify this parameter as string it will be considered as web link and will be used for all transformations.
108 | * For example: 'http://www.myproject.com/materials/default/image.jpg'
109 | * If you specify this parameter as an array, its key will be considered as transformation name, while value - as web link.
110 | * For example:
111 | *
112 | * ```php
113 | * [
114 | * 'full' => 'http://www.myproject.com/materials/default/full.jpg',
115 | * 'thumbnail' => 'http://www.myproject.com/materials/default/thumbnail.jpg',
116 | * ]
117 | * ```
118 | */
119 | public $defaultFileUrl = [];
120 |
121 | /**
122 | * @var string name of the file transformation, which should be used by default, if no specific transformation name given.
123 | */
124 | private $_defaultFileTransformName;
125 |
126 |
127 | /**
128 | * @param string $defaultFileTransformName name of the default file transformation.
129 | */
130 | public function setDefaultFileTransformName($defaultFileTransformName)
131 | {
132 | $this->_defaultFileTransformName = $defaultFileTransformName;
133 | }
134 |
135 | /**
136 | * @return string name of the default file transformation.
137 | */
138 | public function getDefaultFileTransformName()
139 | {
140 | if (empty($this->_defaultFileTransformName)) {
141 | $this->_defaultFileTransformName = $this->initDefaultFileTransformName();
142 | }
143 | return $this->_defaultFileTransformName;
144 | }
145 |
146 | /**
147 | * Initializes the default [[defaultFileTransform]] value.
148 | * @return string transformation name.
149 | */
150 | protected function initDefaultFileTransformName()
151 | {
152 | $fileTransformations = $this->ensureFileTransforms();
153 | if (isset($fileTransformations[0])) {
154 | return $fileTransformations[0];
155 | }
156 | $transformNames = array_keys($fileTransformations);
157 | return array_shift($transformNames);
158 | }
159 |
160 | /**
161 | * Returns the default file URL.
162 | * @param string $name file transformation name.
163 | * @return string default file URL.
164 | */
165 | public function getDefaultFileUrl($name = null)
166 | {
167 | if (empty($this->defaultFileUrl)) {
168 | return null;
169 | }
170 |
171 | if (is_array($this->defaultFileUrl)) {
172 | if (isset($this->defaultFileUrl[$name])) {
173 | return $this->defaultFileUrl[$name];
174 | }
175 | reset($this->defaultFileUrl);
176 | return current($this->defaultFileUrl);
177 | }
178 |
179 | return $this->defaultFileUrl;
180 | }
181 |
182 | /**
183 | * Creates file itself name (without path) including version and extension.
184 | * This method overrides parent implementation in order to include transformation name.
185 | * @param string $fileTransformName image transformation name.
186 | * @param int $fileVersion file version number.
187 | * @param string $fileExtension file extension.
188 | * @return string file self name.
189 | */
190 | public function getFileSelfName($fileTransformName = null, $fileVersion = null, $fileExtension = null)
191 | {
192 | $fileTransformName = $this->fetchFileTransformName($fileTransformName);
193 | $fileNamePrefix = '_' . $fileTransformName;
194 | if (is_null($fileVersion)) {
195 | $fileVersion = $this->getCurrentFileVersion();
196 | }
197 |
198 | $fileExtension = $this->getActualFileExtension($fileExtension, $fileTransformName);
199 |
200 | return $this->getFileBaseName() . $fileNamePrefix . '_' . $fileVersion . '.' . $fileExtension;
201 | }
202 |
203 | /**
204 | * Returns actual file extension for the particular transformation taking in account value of [[transformationFileExtensions]].
205 | * @param string|null $fileExtension original file extension.
206 | * @param string $fileTransformName file transformation name.
207 | * @return string actual file extension to be used.
208 | * @since 1.0.2
209 | */
210 | private function getActualFileExtension($fileExtension, $fileTransformName)
211 | {
212 | if ($fileExtension === null) {
213 | $fileExtension = $this->owner->getAttribute($this->fileExtensionAttribute);
214 | }
215 |
216 | if ($this->transformationFileExtensions === null) {
217 | return $fileExtension;
218 | }
219 |
220 | if (is_callable($this->transformationFileExtensions)) {
221 | return call_user_func($this->transformationFileExtensions, $fileExtension, $fileTransformName);
222 | }
223 |
224 | if (isset($this->transformationFileExtensions[$fileTransformName])) {
225 | if (is_string($this->transformationFileExtensions[$fileTransformName])) {
226 | return $this->transformationFileExtensions[$fileTransformName];
227 | }
228 | return call_user_func($this->transformationFileExtensions[$fileTransformName], $fileExtension);
229 | }
230 |
231 | return $fileExtension;
232 | }
233 |
234 | /**
235 | * Creates the file name in the file storage.
236 | * This name contains the sub directory, resolved by [[subDirTemplate]].
237 | * @param string $fileTransformName file transformation name.
238 | * @param int $fileVersion file version number.
239 | * @param string $fileExtension file extension.
240 | * @return string file full name.
241 | */
242 | public function getFileFullName($fileTransformName = null, $fileVersion = null, $fileExtension = null)
243 | {
244 | $fileName = $this->getFileSelfName($fileTransformName, $fileVersion, $fileExtension);
245 | $subDir = $this->getActualSubDir();
246 | if (!empty($subDir)) {
247 | $fileName = $subDir . DIRECTORY_SEPARATOR . $fileName;
248 | }
249 | return $fileName;
250 | }
251 |
252 | /**
253 | * Fetches the value of file transform name.
254 | * Returns default file transform name if null incoming one is given.
255 | * @param string|null $fileTransformName file transforms name.
256 | * @return string actual file transform name.
257 | */
258 | protected function fetchFileTransformName($fileTransformName = null)
259 | {
260 | if (is_null($fileTransformName)) {
261 | $fileTransformName = $this->getDefaultFileTransformName();
262 | }
263 | return $fileTransformName;
264 | }
265 |
266 | /**
267 | * Returns the [[fileTransformations]] value, making sure it is valid.
268 | * @throws InvalidConfigException if file transforms value is invalid.
269 | * @return array file transforms.
270 | */
271 | protected function ensureFileTransforms()
272 | {
273 | $fileTransformations = $this->fileTransformations;
274 | if (empty($fileTransformations)) {
275 | throw new InvalidConfigException('File transformations list is empty.');
276 | }
277 | return $fileTransformations;
278 | }
279 |
280 | /**
281 | * Overridden.
282 | * Creates the file for the model from the source file.
283 | * File version and extension are passed to this method.
284 | * Parent method is overridden in order to save several different files
285 | * per one particular model.
286 | * @param string $sourceFileName - source full file name.
287 | * @param int $fileVersion - file version number.
288 | * @param string $fileExtension - file extension.
289 | * @return bool success.
290 | */
291 | protected function newFile($sourceFileName, $fileVersion, $fileExtension)
292 | {
293 | $fileTransformations = $this->ensureFileTransforms();
294 |
295 | $fileStorageBucket = $this->ensureFileStorageBucket();
296 | $result = true;
297 | foreach ($fileTransformations as $fileTransformName => $fileTransform) {
298 | if (!is_array($fileTransform) && is_numeric($fileTransformName)) {
299 | $fileTransformName = $fileTransform;
300 | }
301 |
302 | $fileFullName = $this->getFileFullName($fileTransformName, $fileVersion, $fileExtension);
303 |
304 | if (is_array($fileTransform)) {
305 | $transformTempFilePath = $this->ensureTransformationTempFilePath();
306 | $tempTransformFileName = basename($fileFullName);
307 | $tempTransformFileName = uniqid(rand()) . '_' . $tempTransformFileName;
308 | $tempTransformFileName = $transformTempFilePath . DIRECTORY_SEPARATOR . $tempTransformFileName;
309 | $resizeResult = $this->transformFile($sourceFileName, $tempTransformFileName, $fileTransform);
310 | if ($resizeResult) {
311 | $copyResult = $fileStorageBucket->copyFileIn($tempTransformFileName, $fileFullName);
312 | $result = $result && $copyResult;
313 | } else {
314 | $result = $result && $resizeResult;
315 | }
316 | if (file_exists($tempTransformFileName)) {
317 | unlink($tempTransformFileName);
318 | }
319 | } else {
320 | $copyResult = $fileStorageBucket->copyFileIn($sourceFileName, $fileFullName);
321 | $result = $result && $copyResult;
322 | }
323 | }
324 | return $result;
325 | }
326 |
327 | /**
328 | * Ensures [[transformationTempFilePath]] exist and is writeable.
329 | * @throws InvalidConfigException if fails.
330 | * @return string temporary full file path.
331 | */
332 | protected function ensureTransformationTempFilePath()
333 | {
334 | if ($this->transformationTempFilePath === null) {
335 | $filePath = Yii::getAlias('@runtime') . DIRECTORY_SEPARATOR . StringHelper::basename(get_class($this)) . DIRECTORY_SEPARATOR . StringHelper::basename(get_class($this->owner));
336 | $this->transformationTempFilePath = $filePath;
337 | } else {
338 | $filePath = Yii::getAlias($this->transformationTempFilePath);
339 | }
340 |
341 | if (!FileHelper::createDirectory($filePath)) {
342 | throw new InvalidConfigException("Unable to resolve temporary file path: '{$filePath}'!");
343 | }
344 |
345 | return $filePath;
346 | }
347 |
348 | /**
349 | * Overridden.
350 | * Removes all files associated with the owner model.
351 | * @return bool success.
352 | */
353 | public function deleteFile()
354 | {
355 | $fileTransformations = $this->ensureFileTransforms();
356 | $result = true;
357 | $fileStorageBucket = $this->ensureFileStorageBucket();
358 | foreach ($fileTransformations as $fileTransformName => $fileTransform) {
359 | if (!is_array($fileTransform) && is_numeric($fileTransformName)) {
360 | $fileTransformName = $fileTransform;
361 | }
362 | $fileName = $this->getFileFullName($fileTransformName);
363 | if ($fileStorageBucket->fileExists($fileName)) {
364 | $fileDeleteResult = $fileStorageBucket->deleteFile($fileName);
365 | $result = $result && $fileDeleteResult;
366 | }
367 | }
368 | return $result;
369 | }
370 |
371 | /**
372 | * Transforms source file to destination file according to the transformation settings.
373 | * @param string $sourceFileName is the full source file system name.
374 | * @param string $destinationFileName is the full destination file system name.
375 | * @param mixed $transformationSettings is the transform settings data, its value is retrieved from [[fileTransformations]]
376 | * @return bool success.
377 | */
378 | protected function transformFile($sourceFileName, $destinationFileName, $transformationSettings)
379 | {
380 | $arguments = func_get_args();
381 | return call_user_func_array($this->transformCallback, $arguments);
382 | }
383 |
384 | /**
385 | * Re-saves associated file, regenerating all available file transformations.
386 | * This method is useful in case settings for some transformations have been changed and you need to update existing records.
387 | * Note that this method will increment the file version.
388 | * @param string|null $sourceTransformationName name of the file transformation, which should be used as source file,
389 | * if not set - default transformation will be used.
390 | * @return bool success.
391 | * @since 1.0.2
392 | */
393 | public function regenerateFileTransformations($sourceTransformationName = null)
394 | {
395 | $fileFullName = $this->getFileFullName($sourceTransformationName);
396 | $fileStorageBucket = $this->ensureFileStorageBucket();
397 |
398 | $tmpFileName = tempnam(Yii::getAlias('@runtime'), 'tmp_' . StringHelper::basename(get_class($this->owner)) . '_') . '.' . $this->owner->getAttribute($this->fileExtensionAttribute);
399 | $fileStorageBucket->copyFileOut($fileFullName, $tmpFileName);
400 | return $this->saveFile($tmpFileName, true);
401 | }
402 |
403 | // File Interface Function Shortcuts:
404 |
405 | /**
406 | * Checks if file related to the model exists.
407 | * @param string $name transformation name
408 | * @return bool file exists.
409 | */
410 | public function fileExists($name = null)
411 | {
412 | $fileStorageBucket = $this->ensureFileStorageBucket();
413 | return $fileStorageBucket->fileExists($this->getFileFullName($name));
414 | }
415 |
416 | /**
417 | * Returns the content of the model related file.
418 | * @param string $name transformation name
419 | * @return string file content.
420 | */
421 | public function getFileContent($name = null)
422 | {
423 | $fileStorageBucket = $this->ensureFileStorageBucket();
424 | return $fileStorageBucket->getFileContent($this->getFileFullName($name));
425 | }
426 |
427 | /**
428 | * Returns full web link to the model's file.
429 | * @param string $name transformation name
430 | * @return string web link to file.
431 | */
432 | public function getFileUrl($name = null)
433 | {
434 | $fileStorageBucket = $this->ensureFileStorageBucket();
435 | $fileFullName = $this->getFileFullName($name);
436 | $defaultFileUrl = $this->getDefaultFileUrl($name);
437 | if (!empty($defaultFileUrl)) {
438 | if (!$fileStorageBucket->fileExists($fileFullName)) {
439 | return $defaultFileUrl;
440 | }
441 | }
442 | return $fileStorageBucket->getFileUrl($fileFullName);
443 | }
444 |
445 | /**
446 | * Opens a file as stream resource, e.g. like `fopen()` function.
447 | * @param string $mode - the type of access you require to the stream, e.g. `r`, `w`, `a` and so on.
448 | * You should prefer usage of simple modes like `r` and `w`, avoiding complex ones like `w+`, as they
449 | * may not supported by some storages.
450 | * @param string $name transformation name
451 | * @return resource|false file pointer resource on success, or `false` on error.
452 | * @since 1.0.3
453 | */
454 | public function openFile($mode, $name = null)
455 | {
456 | $fileStorageBucket = $this->ensureFileStorageBucket();
457 | return $fileStorageBucket->openFile($this->getFileFullName($name), $mode);
458 | }
459 | }
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "yii2tech/ar-file",
3 | "description": "Provides support for ActiveRecord file attachment",
4 | "keywords": ["yii2", "active", "record", "file", "attachment", "transformation", "image", "thumbnail"],
5 | "type": "yii2-extension",
6 | "license": "BSD-3-Clause",
7 | "support": {
8 | "issues": "https://github.com/yii2tech/ar-file/issues",
9 | "forum": "http://www.yiiframework.com/forum/",
10 | "wiki": "https://github.com/yii2tech/ar-file/wiki",
11 | "source": "https://github.com/yii2tech/ar-file"
12 | },
13 | "authors": [
14 | {
15 | "name": "Paul Klimov",
16 | "email": "klimov.paul@gmail.com"
17 | }
18 | ],
19 | "require": {
20 | "yiisoft/yii2": "*",
21 | "yii2tech/file-storage": "*"
22 | },
23 | "repositories": [
24 | {
25 | "type": "composer",
26 | "url": "https://asset-packagist.org"
27 | }
28 | ],
29 | "suggest": {
30 | "yiisoft/yii2-imagine": "required for `ImageFileBehavior`"
31 | },
32 | "autoload": {
33 | "psr-4": { "yii2tech\\ar\\file\\": "" }
34 | },
35 | "extra": {
36 | "branch-alias": {
37 | "dev-master": "1.0.x-dev"
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------