1<?php 2namespace go\core\orm; 3 4use go\core\model\Link; 5 6/** 7 * Entities can use this trait to make it show up in the global search function 8 * 9 * @property array $customFields 10 */ 11trait SearchableTrait { 12 13 public static $updateSearch = true; 14 15 /** 16 * The description in the search results 17 * 18 * @return string 19 */ 20 abstract protected function getSearchDescription(); 21 22 /** 23 * All the keywords that can be searched on. 24 * 25 * Note: for larger text fields it might be useful to use {@see self::splitTextKeywords()} on it. 26 * 27 * @return string[] 28 */ 29 protected function getSearchKeywords() { 30 return null; 31 } 32 33 /** 34 * You can return an optional search filter here. 35 * 36 * @return string 37 */ 38 protected function getSearchFilter() { 39 return null; 40 } 41 42 /** 43 * Split text by non word characters to get useful search keywords. 44 * @param $text 45 * @return array|false|string[] 46 */ 47 public static function splitTextKeywords($text) { 48 mb_internal_encoding("UTF-8"); 49 mb_regex_encoding("UTF-8"); 50// $split = preg_split('/[^\w\-_\+\\\\\/:]/', mb_strtolower($text), 0, PREG_SPLIT_NO_EMPTY); 51 return mb_split('[^\w\-_\+\\\\\/:]', mb_strtolower($text), -1); 52 } 53 54 /** 55 * Save entity to search cache 56 * 57 * @param bool $checkExisting If certain there's no existing record then this can be set to false 58 * @return bool 59 * @throws \Exception 60 */ 61 public function saveSearch($checkExisting = true) { 62 63 if(!static::$updateSearch) { 64 return true; 65 } 66 67 $search = $checkExisting ? \go\core\model\Search::find()->where('entityTypeId','=', static::entityType()->getId())->andWhere('entityId', '=', $this->id)->single() : false; 68 if(!$search) { 69 $search = new \go\core\model\Search(); 70 $search->setEntity(static::entityType()); 71 } 72 73 if(empty($this->id)) { 74 throw new \Exception("ID is not set"); 75 } 76 77 $search->entityId = $this->id; 78 $search->setAclId($this->findAclId()); 79 $search->name = $this->title(); 80 $search->description = $this->getSearchDescription(); 81 $search->filter = $this->getSearchFilter(); 82 $search->modifiedAt = property_exists($this, 'modifiedAt') ? $this->modifiedAt : new \DateTime(); 83 84// $search->createdAt = $this->createdAt; 85 86 $keywords = $this->getSearchKeywords(); 87 if(!isset($keywords)) { 88 $keywords = array_merge([$search->name], self::splitTextKeywords($search->description)); 89 } 90 91 $links = (new Query()) 92 ->select('description') 93 ->distinct() 94 ->from('core_link') 95 ->where('(toEntityTypeId = :e1 AND toId = :e2)') 96 //->orWhere('(fromEntityTypeId = :e3 AND fromId = :e4)') 97 ->bind([':e1' => static::entityType()->getId(), ':e2' => $this->id]); 98 //':e3' => static::entityType()->getId(), ':e4' => $this->id ]); 99 foreach($links->all() as $link) { 100 if(!empty($link['description']) && is_string($link['description'])) { 101 $keywords[] = $link['description']; 102 } 103 104 } 105 106 if (method_exists($this, 'getCustomFields')) { 107 $keywords = array_merge($keywords, $this->getCustomFieldsSearchKeywords()); 108 } 109 110 $keywords = array_unique($keywords); 111 112 $search->setKeywords(implode(' ', $keywords)); 113 114 if(!$search->internalSave()) { 115 throw new \Exception("Could not save search cache: " . var_export($search->getValidationErrors(), true)); 116 } 117 118 return true; 119 } 120 121 122 123 public static function deleteSearchAndLinks(Query $query) { 124 $delSearchStmt = \go()->getDbConnection() 125 ->delete('core_search', 126 (new Query) 127 ->where(['entityTypeId' => static::entityType()->getId()]) 128 ->andWhere('entityId', 'IN', $query) 129 ); 130// $s = (string) $delSearchStmt; 131 132 if(!$delSearchStmt->execute()) { 133 return false; 134 } 135 136 if(!Link::delete((new Query) 137 ->where(['fromEntityTypeId' => static::entityType()->getId()]) 138 ->andWhere('fromId', 'IN', $query) 139 )) { 140 return false; 141 } 142 143 if(!Link::delete((new Query) 144 ->where(['toEntityTypeId' => static::entityType()->getId()]) 145 ->andWhere('toId', 'IN', $query) 146 )) { 147 return false; 148 } 149 150 return true; 151 } 152 153 154 /** 155 * 156 * @param string $cls 157 * @return \go\core\db\Statement 158 */ 159 private static function queryMissingSearchCache($cls, $offset = 0) { 160 161 $limit = 100; 162 163 $query = $cls::find(); 164 /* @var $query \go\core\db\Query */ 165 $query->join("core_search", "search", "search.entityId = ".$query->getTableAlias() . ".id AND search.entityTypeId = " . $cls::entityType()->getId(), "LEFT"); 166 $query->andWhere('search.id IS NULL') 167 ->limit($limit) 168 ->offset($offset); 169 170 return $query->execute(); 171 } 172 173 private static function rebuildSearchForEntity($cls) { 174 echo $cls."\n"; 175 176 177 echo "Deleting old values\n"; 178 179 $stmt = go()->getDbConnection()->delete('core_search', (new Query) 180 ->where('entityTypeId', '=', $cls::entityType()->getId()) 181 ->andWhere('entityId', 'NOT IN', $cls::find()->selectSingleValue($cls::getMapping()->getPrimaryTable()->getAlias() . '.id')) 182 ); 183 $stmt->execute(); 184 185 echo "Deleted ". $stmt->rowCount() . " entries\n"; 186 187 //In small batches to keep memory low 188 $stmt = self::queryMissingSearchCache($cls); 189 190 $offset = 0; 191 192 //In small batches to keep memory low 193 while($stmt->rowCount()) { 194 195 while ($m = $stmt->fetch()) { 196 197 try { 198 flush(); 199 200 $m->saveSearch(false); 201 echo "."; 202 203 } catch (\Exception $e) { 204 echo "\nError: ". $e->getMessage() ."\n"; 205 \go\core\ErrorHandler::logException($e); 206 207 $offset++; 208 } 209 } 210 echo "\n"; 211 212 $stmt = self::queryMissingSearchCache($cls, $offset); 213 } 214 215 } 216 217 public static function rebuildSearch() { 218 $classFinder = new \go\core\util\ClassFinder(); 219 $entities = $classFinder->findByTrait(SearchableTrait::class); 220 221 foreach($entities as $cls) { 222 self::rebuildSearchForEntity($cls); 223 echo "\nDone\n\n"; 224 } 225 } 226} 227