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