├── 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 | [![Latest Stable Version](https://poser.pugx.org/yii2tech/ar-file/v/stable.png)](https://packagist.org/packages/yii2tech/ar-file) 14 | [![Total Downloads](https://poser.pugx.org/yii2tech/ar-file/downloads.png)](https://packagist.org/packages/yii2tech/ar-file) 15 | [![Build Status](https://travis-ci.org/yii2tech/ar-file.svg?branch=master)](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 | field($model, 'name'); ?> 214 | field($model, 'description'); ?> 215 | 216 | field($model, 'file')->fileInput(); ?> 217 | 218 |
219 | '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 | --------------------------------------------------------------------------------