GalleryBehavior.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. <?php
  2. namespace app\forks\galleryManager;
  3. use Imagine\Image\Box;
  4. use Imagine\Image\ImageInterface;
  5. use yii\base\Behavior;
  6. use yii\base\Exception;
  7. use yii\db\ActiveRecord;
  8. use yii\db\Query;
  9. use yii\helpers\FileHelper;
  10. use yii\imagine\Image;
  11. use app\forks\galleryManager\GalleryImage;
  12. /**
  13. * Behavior for adding gallery to any model.
  14. *
  15. * @author Bogdan Savluk <savluk.bogdan@gmail.com>
  16. *
  17. * @property string $galleryId
  18. */
  19. class GalleryBehavior extends Behavior
  20. {
  21. /**
  22. * Glue used to implode composite primary keys
  23. * @var string
  24. */
  25. public $pkGlue = '_';
  26. /**
  27. * @var string Type name assigned to model in image attachment action
  28. * @see GalleryManagerAction::$types
  29. * @example $type = 'Post' where 'Post' is the model name
  30. */
  31. public $type;
  32. /**
  33. * @var ActiveRecord the owner of this behavior
  34. * @example $owner = Post where Post is the ActiveRecord with GalleryBehavior attached under public function behaviors()
  35. */
  36. public $owner;
  37. /**
  38. * Widget preview height
  39. * @var int
  40. */
  41. public $previewHeight = 200;
  42. /**
  43. * Widget preview width
  44. * @var int
  45. */
  46. public $previewWidth = 200;
  47. /**
  48. * Extension for saved images
  49. * @var string
  50. */
  51. public $extension;
  52. /**
  53. * Path to directory where to save uploaded images
  54. * @var string
  55. */
  56. public $directory;
  57. /**
  58. * Directory Url, without trailing slash
  59. * @var string
  60. */
  61. public $url;
  62. /**
  63. * @var array Functions to generate image versions
  64. * @note Be sure to not modify image passed to your version function,
  65. * because it will be reused in all other versions,
  66. * Before modification you should copy images as in examples below
  67. * @note 'preview' & 'original' versions names are reserved for image preview in widget
  68. * and original image files, if it is required - you can override them
  69. * @example
  70. * [
  71. * 'small' => function ($img) {
  72. * return $img
  73. * ->copy()
  74. * ->resize($img->getSize()->widen(200));
  75. * },
  76. * 'medium' => function ($img) {
  77. * $dstSize = $img->getSize();
  78. * $maxWidth = 800;
  79. * ]
  80. */
  81. public $versions;
  82. /**
  83. * name of query param for modification time hash
  84. * to avoid using outdated version from cache - set it to false
  85. * @var string
  86. */
  87. public $timeHash = '_';
  88. /**
  89. * Used by GalleryManager
  90. * @var bool
  91. * @see GalleryManager::run
  92. */
  93. public $hasName = true;
  94. /**
  95. * Used by GalleryManager
  96. * @var bool
  97. * @see GalleryManager::run
  98. */
  99. public $hasDescription = true;
  100. /**
  101. * @var string Table name for saving gallery images meta information
  102. */
  103. public $tableName = '{{%gallery_image}}';
  104. protected $_galleryId;
  105. /**
  106. * @param ActiveRecord $owner
  107. */
  108. public function attach($owner)
  109. {
  110. parent::attach($owner);
  111. if (!isset($this->versions['original'])) {
  112. $this->versions['original'] = function ($image) {
  113. return $image;
  114. };
  115. }
  116. if (!isset($this->versions['preview'])) {
  117. $this->versions['preview'] = function ($originalImage) {
  118. /** @var ImageInterface $originalImage */
  119. return $originalImage
  120. ->thumbnail(new Box($this->previewWidth, $this->previewHeight));
  121. };
  122. }
  123. }
  124. public function events()
  125. {
  126. return [
  127. ActiveRecord::EVENT_BEFORE_DELETE => 'beforeDelete',
  128. ActiveRecord::EVENT_AFTER_UPDATE => 'afterUpdate',
  129. ActiveRecord::EVENT_AFTER_FIND => 'afterFind',
  130. ];
  131. }
  132. public function beforeDelete()
  133. {
  134. $images = $this->getImages();
  135. foreach ($images as $image) {
  136. $this->deleteImage($image->id);
  137. }
  138. $dirPath = $this->directory . '/' . $this->getGalleryId();
  139. @rmdir($dirPath);
  140. }
  141. public function afterFind()
  142. {
  143. $this->_galleryId = $this->getGalleryId();
  144. }
  145. public function afterUpdate()
  146. {
  147. $galleryId = $this->getGalleryId();
  148. if ($this->_galleryId != $galleryId) {
  149. $dirPath1 = $this->directory . '/' . $this->_galleryId;
  150. $dirPath2 = $this->directory . '/' . $galleryId;
  151. rename($dirPath1, $dirPath2);
  152. }
  153. }
  154. protected $_images = null;
  155. /**
  156. * @return GalleryImage[]
  157. */
  158. public function getImages()
  159. {
  160. if ($this->_images === null) {
  161. $query = new \yii\db\Query();
  162. $imagesData = $query
  163. ->select(['id', 'name', 'description', 'rank'])
  164. ->from($this->tableName)
  165. ->where(['type' => $this->type, 'ownerId' => $this->getGalleryId()])
  166. ->orderBy(['rank' => 'asc'])
  167. ->all();
  168. $this->_images = [];
  169. foreach ($imagesData as $imageData) {
  170. $this->_images[] = new GalleryImage($this, $imageData);
  171. }
  172. }
  173. return $this->_images;
  174. }
  175. protected function getFileName($imageId, $version = 'original')
  176. {
  177. return implode(
  178. '/',
  179. [
  180. $this->getGalleryId(),
  181. $imageId,
  182. $version . '.' . $this->extension,
  183. ]
  184. );
  185. }
  186. public function getUrl($imageId, $version = 'original')
  187. {
  188. $path = $this->getFilePath($imageId, $version);
  189. if (!file_exists($path)) {
  190. if($version=='large'){
  191. return $this->getUrl($imageId, 'medium');
  192. }
  193. return null;
  194. }
  195. if (!empty($this->timeHash)) {
  196. $time = filemtime($path);
  197. $suffix = '?' . $this->timeHash . '=' . crc32($time);
  198. } else {
  199. $suffix = '';
  200. }
  201. return $this->url . '/' . $this->getFileName($imageId, $version) . $suffix;
  202. }
  203. public function getFilePath($imageId, $version = 'original')
  204. {
  205. return $this->directory . '/' . $this->getFileName($imageId, $version);
  206. }
  207. /**
  208. * Replace existing image by specified file
  209. *
  210. * @param $imageId
  211. * @param $path
  212. */
  213. public function replaceImage($imageId, $path)
  214. {
  215. $this->createFolders($this->getFilePath($imageId, 'original'));
  216. $originalImage = Image::getImagine()->open($path);
  217. //save image in original size
  218. //create image preview for gallery manager
  219. foreach ($this->versions as $version => $fn) {
  220. /** @var ImageInterface $image */
  221. $image = call_user_func($fn, $originalImage);
  222. if (is_array($image)) {
  223. list($image, $options) = $image;
  224. } else {
  225. $options = [];
  226. }
  227. $image
  228. ->save($this->getFilePath($imageId, $version), $options);
  229. }
  230. }
  231. private function removeFile($fileName)
  232. {
  233. try {
  234. return FileHelper::unlink($fileName);
  235. } catch (\Exception $ex){
  236. return true;
  237. }
  238. }
  239. /**
  240. * Get Gallery Id
  241. *
  242. * @return mixed as string or integer
  243. * @throws Exception
  244. */
  245. public function getGalleryId()
  246. {
  247. $pk = $this->owner->getPrimaryKey();
  248. if (is_array($pk)) {
  249. return implode($this->pkGlue, $pk);
  250. } else {
  251. return $pk;
  252. }
  253. }
  254. private function createFolders($filePath)
  255. {
  256. return FileHelper::createDirectory(FileHelper::normalizePath(dirname($filePath)), 0777);
  257. }
  258. /////////////////////////////// ========== Public Actions ============ ///////////////////////////
  259. public function deleteImage($imageId)
  260. {
  261. foreach ($this->versions as $version => $fn) {
  262. $filePath = $this->getFilePath($imageId, $version);
  263. $this->removeFile($filePath);
  264. }
  265. $filePath = $this->getFilePath($imageId, 'original');
  266. $parts = explode('/', $filePath);
  267. $parts = array_slice($parts, 0, count($parts) - 1);
  268. $dirPath = implode('/', $parts);
  269. @rmdir($dirPath);
  270. $db = \Yii::$app->db;
  271. $db->createCommand()
  272. ->delete(
  273. $this->tableName,
  274. ['id' => $imageId]
  275. )->execute();
  276. }
  277. public function deleteImages($imageIds)
  278. {
  279. foreach ($imageIds as $imageId) {
  280. $this->deleteImage($imageId);
  281. }
  282. if ($this->_images !== null) {
  283. $removed = array_combine($imageIds, $imageIds);
  284. $this->_images = array_filter(
  285. $this->_images,
  286. function ($image) use (&$removed) {
  287. return !isset($removed[$image->id]);
  288. }
  289. );
  290. }
  291. }
  292. public function addImage($fileName)
  293. {
  294. $db = \Yii::$app->db;
  295. $db->createCommand()
  296. ->insert(
  297. $this->tableName,
  298. [
  299. 'type' => $this->type,
  300. 'ownerId' => $this->getGalleryId()
  301. ]
  302. )->execute();
  303. $id = $db->getLastInsertID('gallery_image_id_seq');
  304. $db->createCommand()
  305. ->update(
  306. $this->tableName,
  307. ['rank' => $id],
  308. ['id' => $id]
  309. )->execute();
  310. $this->replaceImage($id, $fileName);
  311. $galleryImage = new GalleryImage($this, ['id' => $id]);
  312. if ($this->_images !== null) {
  313. $this->_images[] = $galleryImage;
  314. }
  315. return $galleryImage;
  316. }
  317. public function arrange($order)
  318. {
  319. $orders = [];
  320. $i = 0;
  321. foreach ($order as $k => $v) {
  322. if (!$v) {
  323. $order[$k] = $k;
  324. }
  325. $orders[] = $order[$k];
  326. $i++;
  327. }
  328. sort($orders);
  329. $i = 0;
  330. $res = [];
  331. foreach ($order as $k => $v) {
  332. $res[$k] = $orders[$i];
  333. \Yii::$app->db->createCommand()
  334. ->update(
  335. $this->tableName,
  336. ['rank' => $orders[$i]],
  337. ['id' => $k]
  338. )->execute();
  339. $i++;
  340. }
  341. // todo: arrange images if presented
  342. return $order;
  343. }
  344. /**
  345. * @param array $imagesData
  346. *
  347. * @return GalleryImage[]
  348. */
  349. public function updateImagesData($imagesData)
  350. {
  351. $imageIds = array_keys($imagesData);
  352. $imagesToUpdate = [];
  353. if ($this->_images !== null) {
  354. $selected = array_combine($imageIds, $imageIds);
  355. foreach ($this->_images as $img) {
  356. if (isset($selected[$img->id])) {
  357. $imagesToUpdate[] = $selected[$img->id];
  358. }
  359. }
  360. } else {
  361. $rawImages = (new Query())
  362. ->select(['id', 'name', 'description', 'rank'])
  363. ->from($this->tableName)
  364. ->where(['type' => $this->type, 'ownerId' => $this->getGalleryId()])
  365. ->andWhere(['in', 'id', $imageIds])
  366. ->orderBy(['rank' => 'asc'])
  367. ->all();
  368. foreach ($rawImages as $image) {
  369. $imagesToUpdate[] = new GalleryImage($this, $image);
  370. }
  371. }
  372. foreach ($imagesToUpdate as $image) {
  373. if (isset($imagesData[$image->id]['name'])) {
  374. $image->name = $imagesData[$image->id]['name'];
  375. }
  376. if (isset($imagesData[$image->id]['description'])) {
  377. $image->description = $imagesData[$image->id]['description'];
  378. }
  379. \Yii::$app->db->createCommand()
  380. ->update(
  381. $this->tableName,
  382. ['name' => $image->name, 'description' => $image->description],
  383. ['id' => $image->id]
  384. )->execute();
  385. }
  386. return $imagesToUpdate;
  387. }
  388. /**
  389. * Regenerate image versions
  390. * Should be called in migration on every model after changes in versions configuration
  391. *
  392. * @param string|null $oldExtension
  393. */
  394. public function updateImages($oldExtension = null)
  395. {
  396. $ids = array_map(function ($image) {
  397. /** @var GalleryImage $image */
  398. return $image->id;
  399. }, $this->getImages());
  400. foreach ($ids as $id) {
  401. if ($oldExtension !== null) {
  402. $newExtension = $this->extension;
  403. $this->extension = $oldExtension;
  404. $originalImage = Image::getImagine()
  405. ->open($this->getFilePath($id, 'original'));
  406. foreach ($this->versions as $version => $fn) {
  407. $this->removeFile($this->getFilePath($id, $version));
  408. }
  409. $this->extension = $newExtension;
  410. $originalImage->save($this->getFilePath($id, 'original'));
  411. } else {
  412. $originalImage = Image::getImagine()
  413. ->open($this->getFilePath($id, 'original'));
  414. }
  415. foreach ($this->versions as $version => $fn) {
  416. if ($version !== 'original') {
  417. $this->removeFile($this->getFilePath($id, $version));
  418. /** @var ImageInterface $image */
  419. $image = call_user_func($fn, $originalImage);
  420. if (is_array($image)) {
  421. list($image, $options) = $image;
  422. } else {
  423. $options = [];
  424. }
  425. $image->save($this->getFilePath($id, $version), $options);
  426. }
  427. }
  428. }
  429. }
  430. }