Elgg  Version 5.1
SearchService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Search;
4 
6 use Elgg\Config;
7 use Elgg\Database;
15 
23 
27  private $config;
28 
32  private $events;
33 
37  private $db;
38 
40 
48  public function __construct(Config $config, EventsService $events, Database $db) {
49  $this->config = $config;
50  $this->events = $events;
51  $this->db = $db;
52  }
53 
75  public function search(array $options = []) {
77 
78  $query_parts = elgg_extract('query_parts', $options);
79  $fields = (array) elgg_extract('fields', $options);
80 
81  if (empty($query_parts) || empty(array_filter($fields))) {
82  return false;
83  }
84 
85  $entity_type = elgg_extract('type', $options, 'all', false);
86  $entity_subtype = elgg_extract('subtype', $options);
87  $search_type = elgg_extract('search_type', $options, 'entities');
88 
89  if ($entity_type !== 'all' && !in_array($entity_type, Config::ENTITY_TYPES)) {
90  throw new DomainException("'{$entity_type}' is not a valid entity type");
91  }
92 
93  $options = $this->events->triggerResults('search:options', $entity_type, $options, $options);
94  if (!empty($entity_subtype) && is_string($entity_subtype)) {
95  $options = $this->events->triggerResults('search:options', "{$entity_type}:{$entity_subtype}", $options, $options);
96  }
97 
98  $options = $this->events->triggerResults('search:options', $search_type, $options, $options);
99 
100  if ($this->events->hasHandler('search:results', $search_type)) {
101  $results = $this->events->triggerResults('search:results', $search_type, $options);
102  if (isset($results)) {
103  // allow events to conditionally replace the result set
104  return $results;
105  }
106  }
107 
108  return elgg_get_entities($options);
109  }
110 
118  public function normalizeOptions(array $options = []) {
119 
120  if (elgg_extract('_elgg_search_service_normalize_options', $options)) {
121  // already normalized once before
122  return $options;
123  }
124 
125  $search_type = elgg_extract('search_type', $options, 'entities', false);
126  $options['search_type'] = $search_type;
127 
128  $options = $this->events->triggerResults('search:params', $search_type, $options, $options);
129 
130  $options = $this->normalizeQuery($options);
132 
133  // prevent duplicate normalization
134  $options['_elgg_search_service_normalize_options'] = true;
135 
136  return $options;
137  }
138 
146  public function prepareSearchOptions(array $options = []) {
148 
149  $fields = elgg_extract('fields', $options);
150  $query_parts = elgg_extract('query_parts', $options);
151  $partial = elgg_extract('partial_match', $options, true);
152 
153  $options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) {
154  return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial);
155  };
156 
157  return $options;
158  }
159 
167  public function normalizeQuery(array $options = []) {
168 
169  $query = elgg_extract('query', $options, '');
170  $query = strip_tags($query);
171  $query = htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
172  $query = trim($query);
173 
174  $words = preg_split('/\s+/', $query);
175  $words = array_map(function ($e) {
176  return trim($e);
177  }, $words);
178 
179  $query = implode(' ', $words);
180 
181  $options['query'] = $query;
182 
183  $tokenize = elgg_extract('tokenize', $options, true);
184  if ($tokenize) {
185  $parts = $words;
186  } else {
187  $parts = [$query];
188  }
189 
190  $options['query_parts'] = array_unique(array_filter($parts));
191 
192  return $options;
193  }
194 
202  public function normalizeSearchFields(array $options = []) {
203 
204  $default_fields = [
205  'attributes' => [],
206  'metadata' => [],
207  'annotations' => [],
208  ];
209 
210  $fields = $default_fields;
211 
212  $clean_field_property_types = function ($new_fields) use ($default_fields) {
213  $property_types = array_keys($default_fields);
214  foreach ($property_types as $property_type) {
215  if (empty($new_fields[$property_type])) {
216  $new_fields[$property_type] = [];
217  } else {
218  $new_fields[$property_type] = array_unique($new_fields[$property_type]);
219  }
220  }
221 
222  return $new_fields;
223  };
224 
225  $merge_fields = function ($new_fields) use (&$fields, $clean_field_property_types) {
226  if (empty($new_fields) || !is_array($new_fields)) {
227  return;
228  }
229 
230  $new_fields = $clean_field_property_types($new_fields);
231 
232  $fields = array_merge_recursive($fields, $new_fields);
233  };
234 
235  // normalize type/subtype to support all combinations
236  $normalized_options = $this->normalizeTypeSubtypeOptions($options);
237 
238  $type_subtype_pairs = elgg_extract('type_subtype_pairs', $normalized_options);
239  if (!empty($type_subtype_pairs)) {
240  foreach ($type_subtype_pairs as $entity_type => $entity_subtypes) {
241  $result = $this->events->triggerResults('search:fields', $entity_type, $options, $default_fields);
242  $merge_fields($result);
243 
244  if (elgg_is_empty($entity_subtypes)) {
245  continue;
246  }
247 
248  foreach ($entity_subtypes as $entity_subtype) {
249  $result = $this->events->triggerResults('search:fields', "{$entity_type}:{$entity_subtype}", $options, $default_fields);
250  $merge_fields($result);
251  }
252  }
253  }
254 
255  // search fields for search type
256  $search_type = elgg_extract('search_type', $options, 'entities');
257  if ($search_type) {
258  $fields = $this->events->triggerResults('search:fields', $search_type, $options, $fields);
259  }
260 
261  // make sure all supported field types are available
262  $fields = $clean_field_property_types($fields);
263 
264  if (empty($options['fields'])) {
265  $options['fields'] = $fields;
266  } else {
267  // only allow known fields
268  foreach ($fields as $property_type => $property_type_fields) {
269  if (empty($options['fields'][$property_type])) {
270  $options['fields'][$property_type] = [];
271  continue;
272  }
273 
274  $allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]);
275  $options['fields'][$property_type] = array_values(array_unique($allowed));
276  }
277  }
278 
279  return $options;
280  }
281 
293  public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) {
294 
295  $attributes = elgg_extract('attributes', $fields, [], false);
296  $metadata = elgg_extract('metadata', $fields, [], false);
297  $annotations = elgg_extract('annotations', $fields, [], false);
298 
299  $ors = [];
300 
301  $populate_where = function ($where, $part) use ($partial_match) {
302  $where->values = $partial_match ? "%{$part}%" : $part;
303  $where->comparison = 'LIKE';
304  $where->value_type = ELGG_VALUE_STRING;
305  $where->case_sensitive = false;
306  };
307 
308  if (!empty($attributes)) {
309  foreach ($attributes as $attribute) {
310  $attribute_ands = [];
311  foreach ($query_parts as $part) {
312  $where = new AttributeWhereClause();
313  $where->names = $attribute;
314  $populate_where($where, $part);
315  $attribute_ands[] = $where->prepare($qb, $alias);
316  }
317 
318  $ors[] = $qb->merge($attribute_ands, 'AND');
319  }
320  }
321 
322  if (!empty($metadata)) {
323  $metadata_ands = [];
324  $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left');
325  foreach ($query_parts as $part) {
326  $where = new MetadataWhereClause();
327  $populate_where($where, $part);
328  $metadata_ands[] = $where->prepare($qb, $md_alias);
329  }
330 
331  $ors[] = $qb->merge($metadata_ands, 'AND');
332  }
333 
334  if (!empty($annotations)) {
335  $annotations_ands = [];
336  $an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left');
337  foreach ($query_parts as $part) {
338  $where = new AnnotationWhereClause();
339  $populate_where($where, $part);
340  $annotations_ands[] = $where->prepare($qb, $an_alias);
341  }
342 
343  $ors[] = $qb->merge($annotations_ands, 'AND');
344  }
345 
346  return $qb->merge($ors, 'OR');
347  }
348 }
normalizeOptions(array $options=[])
Normalize options.
const ENTITY_TYPES
Definition: Config.php:260
The Elgg database.
Definition: Database.php:25
Builds quereis for matching entities by their attributes.
Exception thrown if a value does not adhere to a defined valid data domain.
Events service.
Database abstraction query builder.
Builds queries for matching annotations against their properties.
normalizeQuery(array $options=[])
Normalize query parts.
$options
Elgg admin footer.
Definition: footer.php:6
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:176
prepareSearchOptions(array $options=[])
Prepare ege* options.
elgg_extract($key, $array, $default=null, bool $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:254
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:504
$fields
Save the configuration of the security.txt contents.
Definition: security_txt.php:6
search(array $options=[])
Returns search results as an array of entities, as a batch, or a count, depending on parameters given...
$results
Definition: content.php:22
joinAnnotationTable($from_alias= '', $from_column= 'guid', $name=null, $join_type= 'inner', $joined_alias=null)
Join annotations table from alias and return joined table alias.
__construct(Config $config, EventsService $events, Database $db)
Constructor.
merge($parts=null, $boolean= 'AND')
Merges multiple composite expressions with a boolean.
const ELGG_VALUE_STRING
Definition: constants.php:112
$metadata
Output annotation metadata.
Definition: metadata.php:9
$query
buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match=true)
Builds search clause.
joinMetadataTable($from_alias= '', $from_column= 'guid', $name=null, $join_type= 'inner', $joined_alias=null)
Join metadata table from alias and return joined table alias.
normalizeSearchFields(array $options=[])
Normalizes an array of search fields.
$qb
Definition: queue.php:11
$attributes
Elgg AJAX loader.
Definition: ajax_loader.php:10
Builds clauses for filtering entities by properties in metadata table.