Elgg  Version master
SearchService.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Search;
4 
5 use Doctrine\DBAL\Query\Expression\CompositeExpression;
6 use Elgg\Config;
7 use Elgg\Database;
14 use Elgg\Traits\Database\LegacyQueryOptionsAdapter;
15 
23 
24  use LegacyQueryOptionsAdapter;
25 
33  public function __construct(
34  protected Config $config,
35  protected EventsService $events,
36  protected Database $db
37  ) {
38  }
39 
61  public function search(array $options = []) {
63 
64  $query_parts = elgg_extract('query_parts', $options);
65  $fields = (array) elgg_extract('fields', $options);
66 
67  if (empty($query_parts) || empty(array_filter($fields))) {
68  return false;
69  }
70 
71  $entity_type = elgg_extract('type', $options, 'all', false);
72  $entity_subtype = elgg_extract('subtype', $options);
73  $search_type = elgg_extract('search_type', $options, 'entities');
74 
75  if ($entity_type !== 'all' && !in_array($entity_type, Config::ENTITY_TYPES)) {
76  throw new DomainException("'{$entity_type}' is not a valid entity type");
77  }
78 
79  $options = $this->events->triggerResults('search:options', $entity_type, $options, $options);
80  if (!empty($entity_subtype) && is_string($entity_subtype)) {
81  $options = $this->events->triggerResults('search:options', "{$entity_type}:{$entity_subtype}", $options, $options);
82  }
83 
84  $options = $this->events->triggerResults('search:options', $search_type, $options, $options);
85 
86  if ($this->events->hasHandler('search:results', $search_type)) {
87  $results = $this->events->triggerResults('search:results', $search_type, $options);
88  if (isset($results)) {
89  // allow events to conditionally replace the result set
90  return $results;
91  }
92  }
93 
95  }
96 
104  public function normalizeOptions(array $options = []) {
105 
106  if (elgg_extract('_elgg_search_service_normalize_options', $options)) {
107  // already normalized once before
108  return $options;
109  }
110 
111  $search_type = elgg_extract('search_type', $options, 'entities', false);
112  $options['search_type'] = $search_type;
113 
114  $options = $this->events->triggerResults('search:params', $search_type, $options, $options);
115 
116  $options = $this->normalizeQuery($options);
118 
119  // prevent duplicate normalization
120  $options['_elgg_search_service_normalize_options'] = true;
121 
122  return $options;
123  }
124 
132  public function prepareSearchOptions(array $options = []) {
134 
135  $fields = elgg_extract('fields', $options);
136  $query_parts = elgg_extract('query_parts', $options);
137  $partial = elgg_extract('partial_match', $options, true);
138 
139  $options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) {
140  return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial);
141  };
142 
143  return $options;
144  }
145 
153  public function normalizeQuery(array $options = []) {
154 
155  $query = elgg_extract('query', $options, '');
156  $query = strip_tags($query);
157  $query = htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8', false);
158  $query = trim($query);
159 
160  $words = preg_split('/\s+/', $query);
161  $words = array_map(function ($e) {
162  return trim($e);
163  }, $words);
164 
165  $query = implode(' ', $words);
166 
167  $options['query'] = $query;
168 
169  $tokenize = elgg_extract('tokenize', $options, true);
170  if ($tokenize) {
171  $parts = $words;
172  } else {
173  $parts = [$query];
174  }
175 
176  $options['query_parts'] = array_unique(array_filter($parts));
177 
178  return $options;
179  }
180 
188  public function normalizeSearchFields(array $options = []) {
189 
190  $default_fields = [
191  'attributes' => [],
192  'metadata' => [],
193  'annotations' => [],
194  ];
195 
196  $fields = $default_fields;
197 
198  $clean_field_property_types = function ($new_fields) use ($default_fields) {
199  $property_types = array_keys($default_fields);
200  foreach ($property_types as $property_type) {
201  if (empty($new_fields[$property_type])) {
202  $new_fields[$property_type] = [];
203  } else {
204  $new_fields[$property_type] = array_unique($new_fields[$property_type]);
205  }
206  }
207 
208  return $new_fields;
209  };
210 
211  $merge_fields = function ($new_fields) use (&$fields, $clean_field_property_types) {
212  if (empty($new_fields) || !is_array($new_fields)) {
213  return;
214  }
215 
216  $new_fields = $clean_field_property_types($new_fields);
217 
218  $fields = array_merge_recursive($fields, $new_fields);
219  };
220 
221  // normalize type/subtype to support all combinations
222  $normalized_options = $this->normalizeTypeSubtypeOptions($options);
223 
224  $type_subtype_pairs = elgg_extract('type_subtype_pairs', $normalized_options);
225  if (!empty($type_subtype_pairs)) {
226  foreach ($type_subtype_pairs as $entity_type => $entity_subtypes) {
227  $result = $this->events->triggerResults('search:fields', $entity_type, $options, $default_fields);
228  $merge_fields($result);
229 
230  if (elgg_is_empty($entity_subtypes)) {
231  continue;
232  }
233 
234  foreach ($entity_subtypes as $entity_subtype) {
235  $result = $this->events->triggerResults('search:fields', "{$entity_type}:{$entity_subtype}", $options, $default_fields);
236  $merge_fields($result);
237  }
238  }
239  }
240 
241  // search fields for search type
242  $search_type = elgg_extract('search_type', $options, 'entities');
243  if ($search_type) {
244  $fields = $this->events->triggerResults('search:fields', $search_type, $options, $fields);
245  }
246 
247  // make sure all supported field types are available
248  $fields = $clean_field_property_types($fields);
249 
250  if (empty($options['fields'])) {
251  $options['fields'] = $fields;
252  } else {
253  // only allow known fields
254  foreach ($fields as $property_type => $property_type_fields) {
255  if (empty($options['fields'][$property_type])) {
256  $options['fields'][$property_type] = [];
257  continue;
258  }
259 
260  $allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]);
261  $options['fields'][$property_type] = array_values(array_unique($allowed));
262  }
263  }
264 
265  return $options;
266  }
267 
279  public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) {
280 
281  $attributes = elgg_extract('attributes', $fields, [], false);
282  $metadata = elgg_extract('metadata', $fields, [], false);
283  $annotations = elgg_extract('annotations', $fields, [], false);
284 
285  $ors = [];
286 
287  $populate_where = function ($where, $part) use ($partial_match) {
288  $where->values = $partial_match ? "%{$part}%" : $part;
289  $where->comparison = 'LIKE';
290  $where->value_type = ELGG_VALUE_STRING;
291  $where->case_sensitive = false;
292  };
293 
294  if (!empty($attributes)) {
295  foreach ($attributes as $attribute) {
296  $attribute_ands = [];
297  foreach ($query_parts as $part) {
298  $where = new AttributeWhereClause();
299  $where->names = $attribute;
300  $populate_where($where, $part);
301  $attribute_ands[] = $where->prepare($qb, $alias);
302  }
303 
304  $ors[] = $qb->merge($attribute_ands, 'AND');
305  }
306  }
307 
308  if (!empty($metadata)) {
309  $metadata_ands = [];
310  $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left');
311  foreach ($query_parts as $part) {
312  $where = new MetadataWhereClause();
313  $populate_where($where, $part);
314  $metadata_ands[] = $where->prepare($qb, $md_alias);
315  }
316 
317  $ors[] = $qb->merge($metadata_ands, 'AND');
318  }
319 
320  if (!empty($annotations)) {
321  $annotations_ands = [];
322  $an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left');
323  foreach ($query_parts as $part) {
324  $where = new AnnotationWhereClause();
325  $populate_where($where, $part);
326  $annotations_ands[] = $where->prepare($qb, $an_alias);
327  }
328 
329  $ors[] = $qb->merge($annotations_ands, 'AND');
330  }
331 
332  return $qb->merge($ors, 'OR');
333  }
334 }
$fields
Save the configuration of the security.txt contents.
Definition: security_txt.php:6
$attributes
Elgg AJAX loader.
Definition: ajax_loader.php:10
$query
const ENTITY_TYPES
Definition: Config.php:267
Builds queries for matching annotations against their properties.
Builds quereis for matching entities by their attributes.
Builds clauses for filtering entities by properties in metadata table.
Database abstraction query builder.
The Elgg database.
Definition: Database.php:26
Events service.
Exception thrown if a value does not adhere to a defined valid data domain.
normalizeSearchFields(array $options=[])
Normalizes an array of search fields.
normalizeOptions(array $options=[])
Normalize options.
normalizeQuery(array $options=[])
Normalize query parts.
search(array $options=[])
Returns search results as an array of entities, as a batch, or a count, depending on parameters given...
buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match=true)
Builds search clause.
__construct(protected Config $config, protected EventsService $events, protected Database $db)
Constructor.
prepareSearchOptions(array $options=[])
Prepare ege* options.
const ELGG_VALUE_STRING
Definition: constants.php:112
if($who_can_change_language==='nobody') elseif($who_can_change_language==='admin_only' &&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
$config
Advanced site settings, debugging section.
Definition: debugging.php:6
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
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:256
elgg_is_empty($value)
Check if a value isn't empty, but allow 0 and '0'.
Definition: input.php:176
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:507
$qb
Definition: queue.php:12
$metadata
Output annotation metadata.
Definition: metadata.php:9
$results
Definition: content.php:22