Elgg  Version 4.3
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;
16 
24 
28  private $config;
29 
33  private $hooks;
34 
38  private $db;
39 
41 
49  public function __construct(Config $config, PluginHooksService $hooks, Database $db) {
50  $this->config = $config;
51  $this->hooks = $hooks;
52  $this->db = $db;
53  }
54 
76  public function search(array $options = []) {
78 
79  $query_parts = elgg_extract('query_parts', $options);
80  $fields = elgg_extract('fields', $options);
81 
82  if (empty($query_parts) || empty(array_filter($fields))) {
83  return false;
84  }
85 
86  $entity_type = elgg_extract('type', $options, 'all', false);
87  $entity_subtype = elgg_extract('subtype', $options);
88  $search_type = elgg_extract('search_type', $options, 'entities');
89 
90  if ($entity_type !== 'all' && !in_array($entity_type, Config::ENTITY_TYPES)) {
91  throw new InvalidParameterException("'$entity_type' is not a valid entity type");
92  }
93 
94  $options = $this->hooks->trigger('search:options', $entity_type, $options, $options);
95  if (!empty($entity_subtype) && is_string($entity_subtype)) {
96  $options = $this->hooks->trigger('search:options', "{$entity_type}:{$entity_subtype}", $options, $options);
97  }
98 
99  $options = $this->hooks->trigger('search:options', $search_type, $options, $options);
100 
101  if ($this->hooks->hasHandler('search:results', $search_type)) {
102  $results = $this->hooks->trigger('search:results', $search_type, $options);
103  if (isset($results)) {
104  // allow hooks to conditionally replace the result set
105  return $results;
106  }
107  }
108 
109  return elgg_get_entities($options);
110  }
111 
119  public function normalizeOptions(array $options = []) {
120 
121  if (elgg_extract('_elgg_search_service_normalize_options', $options)) {
122  // already normalized once before
123  return $options;
124  }
125 
126  $search_type = elgg_extract('search_type', $options, 'entities', false);
127  $options['search_type'] = $search_type;
128 
129  $options = $this->hooks->trigger('search:params', $search_type, $options, $options);
130 
131  $options = $this->normalizeQuery($options);
133 
134  // prevent duplicate normalization
135  $options['_elgg_search_service_normalize_options'] = true;
136 
137  return $options;
138  }
139 
147  public function prepareSearchOptions(array $options = []) {
149 
150  $fields = elgg_extract('fields', $options);
151  $query_parts = elgg_extract('query_parts', $options);
152  $partial = elgg_extract('partial_match', $options, true);
153 
154  $options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) {
155  return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial);
156  };
157 
159 
160  return $options;
161  }
162 
170  public function normalizeQuery(array $options = []) {
171 
172  $query = elgg_extract('query', $options, '');
173  $query = strip_tags($query);
174  $query = htmlspecialchars($query, ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401, 'UTF-8');
175  $query = trim($query);
176 
177  $words = preg_split('/\s+/', $query);
178  $words = array_map(function ($e) {
179  return trim($e);
180  }, $words);
181 
182  $query = implode(' ', $words);
183 
184  $options['query'] = $query;
185 
186  $tokenize = elgg_extract('tokenize', $options, true);
187  if ($tokenize) {
188  $parts = $words;
189  } else {
190  $parts = [$query];
191  }
192 
193  $options['query_parts'] = array_unique(array_filter($parts));
194 
195  return $options;
196  }
197 
205  public function normalizeSearchFields(array $options = []) {
206 
207  $default_fields = [
208  'attributes' => [],
209  'metadata' => [],
210  'annotations' => [],
211  'private_settings' => [],
212  ];
213 
214  $fields = $default_fields;
215 
216  $clean_field_property_types = function ($new_fields) use ($default_fields) {
217  $property_types = array_keys($default_fields);
218  foreach ($property_types as $property_type) {
219  if (empty($new_fields[$property_type])) {
220  $new_fields[$property_type] = [];
221  } else {
222  $new_fields[$property_type] = array_unique($new_fields[$property_type]);
223  }
224  }
225 
226  return $new_fields;
227  };
228 
229  $merge_fields = function ($new_fields) use (&$fields, $clean_field_property_types) {
230  if (empty($new_fields) || !is_array($new_fields)) {
231  return;
232  }
233 
234  $new_fields = $clean_field_property_types($new_fields);
235 
236  $fields = array_merge_recursive($fields, $new_fields);
237  };
238 
239  // normalize type/subtype to support all combinations
240  $normalized_options = $this->normalizeTypeSubtypeOptions($options);
241 
242  $type_subtype_pairs = elgg_extract('type_subtype_pairs', $normalized_options);
243  if (!empty($type_subtype_pairs)) {
244  foreach ($type_subtype_pairs as $entity_type => $entity_subtypes) {
245  $result = $this->hooks->trigger('search:fields', $entity_type, $options, $default_fields);
246  $merge_fields($result);
247 
248  if (elgg_is_empty($entity_subtypes)) {
249  continue;
250  }
251 
252  foreach ($entity_subtypes as $entity_subtype) {
253  $result = $this->hooks->trigger('search:fields', "{$entity_type}:{$entity_subtype}", $options, $default_fields);
254  $merge_fields($result);
255  }
256  }
257  }
258 
259  // search fields for search type
260  $search_type = elgg_extract('search_type', $options, 'entities');
261  if ($search_type) {
262  $fields = $this->hooks->trigger('search:fields', $search_type, $options, $fields);
263  }
264 
265  // make sure all supported field types are available
266  $fields = $clean_field_property_types($fields);
267 
268  if (empty($options['fields'])) {
269  $options['fields'] = $fields;
270  } else {
271  // only allow known fields
272  foreach ($fields as $property_type => $property_type_fields) {
273  if (empty($options['fields'][$property_type])) {
274  $options['fields'][$property_type] = [];
275  continue;
276  }
277 
278  $allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]);
279  $options['fields'][$property_type] = array_values(array_unique($allowed));
280  }
281  }
282 
283  return $options;
284  }
285 
293  public function prepareSortOptions(array $options = []) {
294  $sort = elgg_extract('sort', $options);
295  if (!isset($sort)) {
296  return $options;
297  }
298 
299  elgg_deprecated_notice("Setting the 'sort' option for elgg_search() is deprecated use 'sort_by'", '4.2');
300 
301  if (is_string($sort)) {
302  $sort = [
303  'property' => $sort,
304  ];
305 
306  $order = elgg_extract('order', $options);
307  if (isset($order)) {
308  elgg_deprecated_notice("Setting the 'order' option for elgg_search() is deprecated use 'sort_by'", '4.2');
309 
310  $sort['direction'] = $order;
311  }
312  }
313 
314  $options['sort_by'][] = $sort;
315 
316  return $options;
317  }
318 
330  public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) {
331 
332  $attributes = elgg_extract('attributes', $fields, [], false);
333  $metadata = elgg_extract('metadata', $fields, [], false);
334  $annotations = elgg_extract('annotations', $fields, [], false);
335  $private_settings = elgg_extract('private_settings', $fields, [], false);
336 
337  $ors = [];
338 
339  $populate_where = function ($where, $part) use ($partial_match) {
340  $where->values = $partial_match ? "%{$part}%" : $part;
341  $where->comparison = 'LIKE';
342  $where->value_type = ELGG_VALUE_STRING;
343  $where->case_sensitive = false;
344  };
345 
346  if (!empty($attributes)) {
347  foreach ($attributes as $attribute) {
348  $attribute_ands = [];
349  foreach ($query_parts as $part) {
350  $where = new AttributeWhereClause();
351  $where->names = $attribute;
352  $populate_where($where, $part);
353  $attribute_ands[] = $where->prepare($qb, $alias);
354  }
355  $ors[] = $qb->merge($attribute_ands, 'AND');
356  }
357  }
358 
359  if (!empty($metadata)) {
360  $metadata_ands = [];
361  $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left');
362  foreach ($query_parts as $part) {
363  $where = new MetadataWhereClause();
364  $populate_where($where, $part);
365  $metadata_ands[] = $where->prepare($qb, $md_alias);
366  }
367  $ors[] = $qb->merge($metadata_ands, 'AND');
368  }
369 
370  if (!empty($annotations)) {
371  $annotations_ands = [];
372  $an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left');
373  foreach ($query_parts as $part) {
374  $where = new AnnotationWhereClause();
375  $populate_where($where, $part);
376  $annotations_ands[] = $where->prepare($qb, $an_alias);
377  }
378  $ors[] = $qb->merge($annotations_ands, 'AND');
379  }
380 
381  if (!empty($private_settings)) {
382  $private_settings_ands = [];
383  $ps_alias = $qb->joinPrivateSettingsTable($alias, 'guid', $private_settings, 'left');
384  foreach ($query_parts as $part) {
385  $where = new PrivateSettingWhereClause();
386  $populate_where($where, $part);
387  $private_settings_ands[] = $where->prepare($qb, $ps_alias);
388  }
389  $ors[] = $qb->merge($private_settings_ands, 'AND');
390  }
391 
392  return $qb->merge($ors, 'OR');
393  }
394 
395 }
normalizeOptions(array $options=[])
Normalize options.
elgg_deprecated_notice(string $msg, string $dep_version)
Log a notice about deprecated use of a function, view, etc.
Definition: deprecation.php:52
const ENTITY_TYPES
Definition: Config.php:264
The Elgg database.
Definition: Database.php:25
__construct(Config $config, PluginHooksService $hooks, Database $db)
Constructor.
Builds quereis for matching entities by their attributes.
Database abstraction query builder.
Builds queries for matching annotations against their properties.
prepareSortOptions(array $options=[])
Normalizes sort options.
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:179
prepareSearchOptions(array $options=[])
Prepare ege* options.
if(!$entity instanceof\ElggUser) $fields
Definition: profile.php:14
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:545
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.
elgg_extract($key, $array, $default=null, $strict=true)
Checks for $array[$key] and returns its value if it exists, else returns $default.
Definition: elgglib.php:547
joinPrivateSettingsTable($from_alias= '', $from_column= 'guid', $name=null, $join_type= 'inner', $joined_alias=null)
Join private settings table from alias and return joined table alias.
merge($parts=null, $boolean= 'AND')
Merges multiple composite expressions with a boolean.
const ELGG_VALUE_STRING
Definition: constants.php:127
$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.
Builds queries for filtering entties by their properties in private_settings table.
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.