PageRenderTime 58ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/protected/modules/tag/components/ETaggableBehavior.php

https://bitbucket.org/rohitrox/hotc
PHP | 688 lines | 390 code | 61 blank | 237 comment | 42 complexity | c7a951dc2285713bd7856bd6db92604a MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. /**
  3. * ETaggableBehavior class file.
  4. *
  5. * @author Alexander Makarov
  6. * @link http://code.google.com/p/yiiext/
  7. */
  8. /**
  9. * Provides tagging ability for a model.
  10. *
  11. * @version 1.5
  12. * @package yiiext.behaviors.model.taggable
  13. */
  14. class ETaggableBehavior extends CActiveRecordBehavior {
  15. /**
  16. * @var string tags table name.
  17. */
  18. public $tagTable = 'tag';
  19. /**
  20. * @var string tag table field that contains tag name.
  21. */
  22. public $tagTableName = 'name';
  23. /**
  24. * @var string tag table PK name.
  25. */
  26. public $tagTablePk = 'id';
  27. /**
  28. * @var string tag to Model binding table name.
  29. * Defaults to `{model table name}Tag`.
  30. */
  31. public $tagBindingTable = 'page_nm_tag';
  32. /**
  33. * @var string binding table tagId name.
  34. */
  35. public $tagBindingTableTagId = 'tag_id';
  36. /**
  37. * @var string|null tag table count field. If null don't uses database.
  38. */
  39. public $tagTableCount;
  40. /**
  41. * @var string binding table model FK name.
  42. * Defaults to `{model table name with first lowercased letter}Id`.
  43. */
  44. public $modelTableFk = 'page_id';
  45. /**
  46. * @var boolean which create tags automatically or throw exception if tag does not exist.
  47. */
  48. public $createTagsAutomatically = true;
  49. /**
  50. * @var string|boolean caching component Id. If false don't use cache.
  51. * Defaults to false.
  52. */
  53. public $cacheID = 'cache';
  54. private $tags = array();
  55. private $originalTags = array();
  56. /**
  57. * @var CDbConnection
  58. */
  59. private $_conn;
  60. /**
  61. * @var CCache
  62. */
  63. protected $cache;
  64. /**
  65. * @var array|CDbCriteria default scope criteria. Used as filter in find, load, create or update tags.
  66. */
  67. public $scope = array();
  68. /**
  69. * @var array these values are added on inserting tag into DB.
  70. */
  71. public $insertValues = array();
  72. /**
  73. * @var CDbCriteria|null scope CDbCriteria cache.
  74. */
  75. private $scopeCriteria = null;
  76. /**
  77. * Get DB connection.
  78. * @return CDbConnection
  79. */
  80. protected function getConnection() {
  81. if(!isset($this->_conn)){
  82. $this->_conn = $this->getOwner()->dbConnection;
  83. }
  84. return $this->_conn;
  85. }
  86. /**
  87. * @throws CException
  88. * @param CComponent $owner
  89. * @return void
  90. */
  91. public function attach($owner) {
  92. // Prepare cache component
  93. if($this->cacheID!==false)
  94. $this->cache = Yii::app()->getComponent($this->cacheID);
  95. if(!($this->cache instanceof ICache)){
  96. // If not set cache component, use dummy cache.
  97. $this->cache = new CDummyCache;
  98. }
  99. parent::attach($owner);
  100. }
  101. /**
  102. * Allows to print object.
  103. * @return string
  104. */
  105. public function toString() {
  106. $this->loadTags();
  107. return implode(', ', $this->tags);
  108. }
  109. /**
  110. * Get tag binding table name.
  111. * @access private
  112. * @return string
  113. */
  114. private function getTagBindingTableName() {
  115. if($this->tagBindingTable === null){
  116. $this->tagBindingTable = $this->getOwner()->tableName().'Tag';
  117. }
  118. return $this->tagBindingTable;
  119. }
  120. /**
  121. * Get model table FK name.
  122. * @access private
  123. * @return string
  124. */
  125. private function getModelTableFkName() {
  126. if($this->modelTableFk === null){
  127. $tableName = $this->getOwner()->tableName();
  128. $tableName[0] = strtolower($tableName[0]);
  129. $this->modelTableFk = $tableName.'Id';
  130. }
  131. return $this->modelTableFk;
  132. }
  133. /**
  134. * Set one or more tags.
  135. * @param string|array $tags
  136. * @return void
  137. */
  138. public function setTags($tags) {
  139. $tags = $this->toTagsArray($tags);
  140. $this->tags = array_unique($tags);
  141. return $this->getOwner();
  142. }
  143. /**
  144. * Add one or more tags.
  145. * @param string|array $tags
  146. * @return void
  147. */
  148. public function addTags($tags) {
  149. $this->loadTags();
  150. $tags = $this->toTagsArray($tags);
  151. $this->tags = array_unique(array_merge($this->tags, $tags));
  152. return $this->getOwner();
  153. }
  154. /**
  155. * Alias of {@link addTags()}.
  156. * @param string|array $tags
  157. * @return void
  158. */
  159. public function addTag($tags) {
  160. return $this->addTags($tags);
  161. }
  162. /**
  163. * Remove one or more tags.
  164. * @param string|array $tags
  165. * @return void
  166. */
  167. public function removeTags($tags) {
  168. $this->loadTags();
  169. $tags = $this->toTagsArray($tags);
  170. $this->tags = array_diff($this->tags, $tags);
  171. return $this->getOwner();
  172. }
  173. /**
  174. * Alias of {@link removeTags}.
  175. * @param string|array $tags
  176. * @return void
  177. */
  178. public function removeTag($tags) {
  179. return $this->removeTags($tags);
  180. }
  181. /**
  182. * Remove all tags.
  183. * @return void
  184. */
  185. public function removeAllTags() {
  186. $this->loadTags();
  187. $this->tags = array();
  188. return $this->getOwner();
  189. }
  190. /**
  191. * Get default scope criteria.
  192. * @return CDbCriteria
  193. */
  194. protected function getScopeCriteria() {
  195. if(!$this->scopeCriteria){
  196. $scope = $this->scope;
  197. if(is_array($this->scope) && !empty($this->scope)){
  198. $scope = new CDbCriteria($this->scope);
  199. }
  200. if($scope instanceof CDbCriteria){
  201. $this->scopeCriteria = $scope;
  202. }
  203. }
  204. return $this->scopeCriteria;
  205. }
  206. /**
  207. * Get tags.
  208. * @return array
  209. */
  210. public function getTags() {
  211. $this->loadTags();
  212. return $this->tags;
  213. }
  214. /**
  215. * Get current model's tags with counts.
  216. * @todo: quick implementation, rewrite!
  217. * @param CDbCriteria $criteria
  218. * @return array
  219. */
  220. public function getTagsWithModelsCount($criteria = null) {
  221. if(!($tags = $this->cache->get($this->getCacheKey().'WithModelsCount'))){
  222. $builder = $this->getConnection()->getCommandBuilder();
  223. if($this->tagTableCount !== null){
  224. $findCriteria = new CDbCriteria(array(
  225. 'select' => "t.{$this->tagTableName} as `name`, t.{$this->tagTableCount} as `count` ",
  226. 'join' => "INNER JOIN {$this->getTagBindingTableName()} et on t.{$this->tagTablePk} = et.{$this->tagBindingTableTagId} ",
  227. 'condition' => "et.{$this->getModelTableFkName()} = :ownerid ",
  228. 'params' => array(
  229. ':ownerid' => $this->getOwner()->primaryKey,
  230. )
  231. ));
  232. } else{
  233. $findCriteria = new CDbCriteria(array(
  234. 'select' => "t.{$this->tagTableName} as `name`, count(*) as `count` ",
  235. 'join' => "INNER JOIN {$this->getTagBindingTableName()} et on t.{$this->tagTablePk} = et.{$this->tagBindingTableTagId} ",
  236. 'condition' => "et.{$this->getModelTableFkName()} = :ownerid ",
  237. 'group' => 't.'.$this->tagTablePk,
  238. 'params' => array(
  239. ':ownerid' => $this->getOwner()->primaryKey,
  240. )
  241. ));
  242. }
  243. if($criteria){
  244. $findCriteria->mergeWith($criteria);
  245. }
  246. $tags = $builder->createFindCommand(
  247. $this->tagTable,
  248. $findCriteria
  249. )->queryAll();
  250. $this->cache->set($this->getCacheKey().'WithModelsCount', $tags);
  251. }
  252. return $tags;
  253. }
  254. /**
  255. * Get tags array from comma separated tags string.
  256. * @access private
  257. * @param string|array $tags
  258. * @return array
  259. */
  260. protected function toTagsArray($tags) {
  261. if(!is_array($tags)){
  262. $tags = explode(',', trim(strip_tags($tags), ' ,'));
  263. }
  264. array_walk($tags, array($this, 'trim'));
  265. return $tags;
  266. }
  267. /**
  268. * Used as a callback to trim tags.
  269. * @access private
  270. * @param string $item
  271. * @param string $key
  272. * @return string
  273. */
  274. private function trim(&$item, $key) {
  275. $item = trim($item);
  276. }
  277. /**
  278. * If we need to save tags.
  279. * @access private
  280. * @return boolean
  281. */
  282. private function needToSave() {
  283. $diff = array_merge(
  284. array_diff($this->tags, $this->originalTags),
  285. array_diff($this->originalTags, $this->tags)
  286. );
  287. return !empty($diff);
  288. }
  289. /**
  290. * Saves model tags on model save.
  291. * @param CModelEvent $event
  292. * @throw Exception
  293. */
  294. public function afterSave($event) {
  295. if($this->needToSave()){
  296. $builder = $this->getConnection()->getCommandBuilder();
  297. if(!$this->createTagsAutomatically){
  298. // checking if all of the tags are existing ones
  299. foreach($this->tags as $tag){
  300. $findCriteria = new CDbCriteria(array(
  301. 'select' => "t.".$this->tagTablePk,
  302. 'condition' => "t.{$this->tagTableName} = :tag ",
  303. 'params' => array(':tag' => $tag),
  304. ));
  305. if($this->getScopeCriteria()){
  306. $findCriteria->mergeWith($this->getScopeCriteria());
  307. }
  308. $tagId = $builder->createFindCommand(
  309. $this->tagTable,
  310. $findCriteria
  311. )->queryScalar();
  312. if(!$tagId){
  313. throw new Exception("Tag \"$tag\" does not exist. Please add it before assigning or enable createTagsAutomatically.");
  314. }
  315. }
  316. }
  317. if(!$this->getOwner()->getIsNewRecord()){
  318. // delete all present tag bindings if record is existing one
  319. $this->deleteTags();
  320. }
  321. // add new tag bindings and tags if there are any
  322. if(!empty($this->tags)){
  323. foreach($this->tags as $tag){
  324. if(empty($tag)) return;
  325. // try to get existing tag
  326. $findCriteria = new CDbCriteria(array(
  327. 'select' => "t.".$this->tagTablePk,
  328. 'condition' => "t.{$this->tagTableName} = :tag ",
  329. 'params' => array(':tag' => $tag),
  330. ));
  331. if($this->getScopeCriteria()){
  332. $findCriteria->mergeWith($this->getScopeCriteria());
  333. }
  334. $tagId = $builder->createFindCommand(
  335. $this->tagTable,
  336. $findCriteria
  337. )->queryScalar();
  338. // if there is no existing tag, create one
  339. if(!$tagId){
  340. $this->createTag($tag);
  341. // reset all tags cache
  342. $this->resetAllTagsCache();
  343. $this->resetAllTagsWithModelsCountCache();
  344. $tagId = $this->getConnection()->getLastInsertID();
  345. }
  346. // bind tag to it's model
  347. $builder->createInsertCommand(
  348. $this->getTagBindingTableName(),
  349. array(
  350. $this->getModelTableFkName() => $this->getOwner()->primaryKey,
  351. $this->tagBindingTableTagId => $tagId
  352. )
  353. )->execute();
  354. }
  355. $this->updateCount(+1);
  356. }
  357. $this->cache->set($this->getCacheKey(), $this->tags);
  358. }
  359. parent::afterSave($event);
  360. }
  361. /**
  362. * Reset cache used for {@link getAllTags()}.
  363. * @return void
  364. */
  365. public function resetAllTagsCache() {
  366. $this->cache->delete('Taggable'.$this->getOwner()->tableName().'All');
  367. }
  368. /**
  369. * Reset cache used for {@link getAllTagsWithModelsCount()}.
  370. * @return void
  371. */
  372. public function resetAllTagsWithModelsCountCache() {
  373. $this->cache->delete('Taggable'.$this->getOwner()->tableName().'AllWithCount');
  374. }
  375. /**
  376. * Deletes tag bindings on model delete.
  377. * @param CModelEvent $event
  378. * @return void
  379. */
  380. public function afterDelete($event) {
  381. // delete all present tag bindings
  382. $this->deleteTags();
  383. $this->cache->delete($this->getCacheKey());
  384. $this->resetAllTagsWithModelsCountCache();
  385. parent::afterDelete($event);
  386. }
  387. /**
  388. * Load tags into model.
  389. * @params array|CDbCriteria $criteria, defaults to null.
  390. * @access protected
  391. * @return void
  392. */
  393. protected function loadTags($criteria = null) {
  394. if($this->tags != null) return;
  395. if($this->getOwner()->getIsNewRecord()) return;
  396. if(!($tags = $this->cache->get($this->getCacheKey()))){
  397. $findCriteria = new CDbCriteria(array(
  398. 'select' => "t.{$this->tagTableName} as `name`",
  399. 'join' => "INNER JOIN {$this->getTagBindingTableName()} et ON t.{$this->tagTablePk} = et.{$this->tagBindingTableTagId} ",
  400. 'condition' => "et.{$this->getModelTableFkName()} = :ownerid ",
  401. 'params' => array(
  402. ':ownerid' => $this->getOwner()->primaryKey,
  403. )
  404. ));
  405. if($criteria){
  406. $findCriteria->mergeWith($criteria);
  407. }
  408. if($this->getScopeCriteria()){
  409. $findCriteria->mergeWith($this->getScopeCriteria());
  410. }
  411. $tags = $this->getConnection()->getCommandBuilder()->createFindCommand(
  412. $this->tagTable,
  413. $findCriteria
  414. )->queryColumn();
  415. $this->cache->set($this->getCacheKey(), $tags);
  416. }
  417. $this->originalTags = $this->tags = $tags;
  418. }
  419. /**
  420. * Returns key for caching specific model tags.
  421. * @return string
  422. */
  423. private function getCacheKey() {
  424. return $this->getCacheKeyBase().$this->getOwner()->primaryKey;
  425. }
  426. /**
  427. * Returns cache key base.
  428. * @return string
  429. */
  430. private function getCacheKeyBase() {
  431. return 'Taggable'.
  432. $this->getOwner()->tableName().
  433. $this->tagTable.
  434. $this->tagBindingTable.
  435. $this->tagTableName.
  436. $this->getModelTableFkName().
  437. $this->tagBindingTableTagId.
  438. json_encode($this->scope);
  439. }
  440. /**
  441. * Get criteria to limit query by tags.
  442. * @access private
  443. * @param array $tags
  444. * @return CDbCriteria
  445. */
  446. protected function getFindByTagsCriteria($tags) {
  447. $criteria = new CDbCriteria();
  448. $pk = $this->getOwner()->tableSchema->primaryKey;
  449. if(!empty($tags)){
  450. $conn = $this->getConnection();
  451. $criteria->select = 't.*';
  452. for($i = 0, $count = count($tags); $i < $count; $i++){
  453. $tag = $conn->quoteValue($tags[$i]);
  454. $criteria->join .=
  455. "JOIN {$this->getTagBindingTableName()} bt$i ON t.{$pk} = bt$i.{$this->getModelTableFkName()}
  456. JOIN {$this->tagTable} tag$i ON tag$i.{$this->tagTablePk} = bt$i.{$this->tagBindingTableTagId} AND tag$i.`{$this->tagTableName}` = $tag";
  457. }
  458. }
  459. if($this->getScopeCriteria()){
  460. $criteria->mergeWith($this->getScopeCriteria());
  461. }
  462. return $criteria;
  463. }
  464. /**
  465. * Get all possible tags for current model class.
  466. * @param CDbCriteria $criteria
  467. * @return array
  468. */
  469. public function getAllTags($criteria = null) {
  470. if(!($tags = $this->cache->get('Taggable'.$this->getOwner()->tableName().'All'))){
  471. // getting associated tags
  472. $builder = $this->getOwner()->getCommandBuilder();
  473. $findCriteria = new CDbCriteria();
  474. $findCriteria->select = $this->tagTableName;
  475. if($criteria){
  476. $findCriteria->mergeWith($criteria);
  477. }
  478. if($this->getScopeCriteria()){
  479. $findCriteria->mergeWith($this->getScopeCriteria());
  480. }
  481. $tags = $builder->createFindCommand($this->tagTable, $findCriteria)->queryColumn();
  482. $this->cache->set('Taggable'.$this->getOwner()->tableName().'All', $tags);
  483. }
  484. return $tags;
  485. }
  486. /**
  487. * Get all possible tags with models count for each for this model class.
  488. * @param CDbCriteria $criteria
  489. * @return array
  490. */
  491. public function getAllTagsWithModelsCount($criteria = null) {
  492. if(!($tags = $this->cache->get('Taggable'.$this->getOwner()->tableName().'AllWithCount'))){
  493. // getting associated tags
  494. $builder = $this->getOwner()->getCommandBuilder();
  495. $tagsCriteria = new CDbCriteria();
  496. if($this->tagTableCount !== null){
  497. $tagsCriteria->select = sprintf(
  498. "t.%s as `name`, %s as `count`",
  499. $this->tagTableName,
  500. $this->tagTableCount
  501. );
  502. }
  503. else{
  504. $tagsCriteria->select = sprintf(
  505. "t.%s as `name`, count(*) as `count`",
  506. $this->tagTableName
  507. );
  508. $tagsCriteria->join = sprintf(
  509. "JOIN `%s` et ON t.{$this->tagTablePk} = et.%s",
  510. $this->getTagBindingTableName(),
  511. $this->tagBindingTableTagId
  512. );
  513. $tagsCriteria->group = 't.'.$this->tagTablePk;
  514. }
  515. if($criteria!==null)
  516. $tagsCriteria->mergeWith($criteria);
  517. if($this->getScopeCriteria())
  518. $tagsCriteria->mergeWith($this->getScopeCriteria());
  519. $tags = $builder->createFindCommand($this->tagTable, $tagsCriteria)->queryAll();
  520. $this->cache->set('Taggable'.$this->getOwner()->tableName().'AllWithCount', $tags);
  521. }
  522. return $tags;
  523. }
  524. /**
  525. * Finds out if model has all tags specified.
  526. * @param string|array $tags
  527. * @return boolean
  528. */
  529. public function hasTags($tags) {
  530. $this->loadTags();
  531. $tags = $this->toTagsArray($tags);
  532. foreach($tags as $tag){
  533. if(!in_array($tag, $this->tags)) return false;
  534. }
  535. return true;
  536. }
  537. /**
  538. * Alias of {@link hasTags()}.
  539. * @param string|array $tags
  540. * @return boolean
  541. */
  542. public function hasTag($tags) {
  543. return $this->hasTags($tags);
  544. }
  545. /**
  546. * Limit current AR query to have all tags specified.
  547. * @param string|array $tags
  548. * @return CActiveRecord
  549. */
  550. public function taggedWith($tags) {
  551. $tags = $this->toTagsArray($tags);
  552. if(!empty($tags)){
  553. $criteria = $this->getFindByTagsCriteria($tags);
  554. $this->getOwner()->getDbCriteria()->mergeWith($criteria);
  555. }
  556. return $this->getOwner();
  557. }
  558. /**
  559. * Alias of {@link taggedWith()}.
  560. * @param string|array $tags
  561. * @return CActiveRecord
  562. */
  563. public function withTags($tags) {
  564. return $this->taggedWith($tags);
  565. }
  566. /**
  567. * Delete all present tag bindings.
  568. * @return void
  569. */
  570. protected function deleteTags() {
  571. $this->updateCount(-1);
  572. $conn = $this->getConnection();
  573. $conn->createCommand(
  574. sprintf(
  575. "DELETE
  576. FROM `%s`
  577. WHERE %s = %d",
  578. $this->getTagBindingTableName(),
  579. $this->getModelTableFkName(),
  580. $this->getOwner()->primaryKey
  581. )
  582. )->execute();
  583. }
  584. /**
  585. * Creates a tag.
  586. * Method is for future inheritance.
  587. * @param string $tag tag name.
  588. * @return void
  589. */
  590. protected function createTag($tag) {
  591. $builder = $this->getConnection()->getCommandBuilder();
  592. $values = array(
  593. $this->tagTableName => $tag
  594. );
  595. if(is_array($this->insertValues)){
  596. $values = array_merge($this->insertValues, $values);
  597. }
  598. $builder->createInsertCommand($this->tagTable, $values)->execute();
  599. }
  600. /**
  601. * Updates counter information in database.
  602. * Used if {@link tagTableCount} is not null.
  603. * @param int $count incremental ("1") or decremental ("-1") value.
  604. * @return void
  605. */
  606. protected function updateCount($count) {
  607. if($this->tagTableCount !== null){
  608. $conn = $this->getConnection();
  609. $conn->createCommand(
  610. sprintf(
  611. "UPDATE %s
  612. SET %s = %s + %s
  613. WHERE %s in (SELECT %s FROM %s WHERE %s = %d)",
  614. $this->tagTable,
  615. $this->tagTableCount,
  616. $this->tagTableCount,
  617. $count,
  618. $this->tagTablePk,
  619. $this->tagBindingTableTagId,
  620. $this->getTagBindingTableName(),
  621. $this->getModelTableFkName(),
  622. $this->getOwner()->primaryKey
  623. )
  624. )->execute();
  625. }
  626. }
  627. }