Elgg  Version 3.0
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;
14 use ElggBatch;
15 use ElggEntity;
18 
31 
35  private $config;
36 
40  private $hooks;
41 
45  private $db;
46 
48 
56  public function __construct(Config $config, PluginHooksService $hooks, Database $db) {
57  $this->config = $config;
58  $this->hooks = $hooks;
59  $this->db = $db;
60  }
61 
83  public function search(array $options = []) {
85 
86  $query_parts = elgg_extract('query_parts', $options);
87  $fields = elgg_extract('fields', $options);
88 
89  if (empty($query_parts) || empty(array_filter($fields))) {
90  return false;
91  }
92 
93  $entity_type = elgg_extract('type', $options, 'all', false);
94  $entity_subtype = elgg_extract('subtype', $options);
95  $search_type = elgg_extract('search_type', $options, 'entities');
96 
97  if ($entity_type !== 'all' && !in_array($entity_type, Config::getEntityTypes())) {
98  throw new InvalidParameterException("'$entity_type' is not a valid entity type");
99  }
100 
101  $options = $this->hooks->trigger('search:options', $entity_type, $options, $options);
102  if ($entity_subtype) {
103  $options = $this->hooks->trigger('search:options', "$entity_type:$entity_subtype", $options, $options);
104  }
105 
106  $options = $this->hooks->trigger('search:options', $search_type, $options, $options);
107 
108  if ($this->hooks->hasHandler('search:results', $search_type)) {
109  $results = $this->hooks->trigger('search:results', $search_type, $options);
110  if (isset($results)) {
111  // allow hooks to conditionally replace the result set
112  return $results;
113  }
114  }
115 
116  return elgg_get_entities($options);
117  }
118 
126  public function normalizeOptions(array $options = []) {
127 
128  if (elgg_extract('_elgg_search_service_normalize_options', $options)) {
129  // already normalized once before
130  return $options;
131  }
132 
133  $search_type = elgg_extract('search_type', $options, 'entities', false);
134  $options['search_type'] = $search_type;
135 
136  $options = $this->hooks->trigger('search:params', $search_type, $options, $options);
137 
138  $options = $this->normalizeQuery($options);
140 
141  // prevent duplicate normalization
142  $options['_elgg_search_service_normalize_options'] = true;
143 
144  return $options;
145  }
146 
154  public function prepareSearchOptions(array $options = []) {
156 
157  $fields = elgg_extract('fields', $options);
158  $query_parts = elgg_extract('query_parts', $options);
159  $partial = elgg_extract('partial_match', $options, true);
160 
161  $options['wheres']['search'] = function (QueryBuilder $qb, $alias) use ($fields, $query_parts, $partial) {
162  return $this->buildSearchWhereQuery($qb, $alias, $fields, $query_parts, $partial);
163  };
164 
166 
167  return $options;
168  }
169 
177  public function normalizeQuery(array $options = []) {
178 
179  $query = elgg_extract('query', $options);
180  $query = filter_var($query, FILTER_SANITIZE_STRING);
181  $query = trim($query);
182 
183  $words = preg_split('/\s+/', $query);
184  $words = array_map(function ($e) {
185  return trim($e);
186  }, $words);
187 
188  $query = implode(' ', $words);
189 
190  $options['query'] = $query;
191 
192  $tokenize = elgg_extract('tokenize', $options, true);
193  if ($tokenize) {
194  $parts = $words;
195  } else {
196  $parts = [$query];
197  }
198 
199  $options['query_parts'] = array_unique(array_filter($parts));
200 
201  return $options;
202  }
203 
211  public function normalizeSearchFields(array $options = []) {
212 
213  $default_fields = [
214  'attributes' => [],
215  'metadata' => [],
216  'annotations' => [],
217  'private_settings' => [],
218  ];
219 
220  $fields = $default_fields;
221 
222  $clean_field_property_types = function ($new_fields) use ($default_fields) {
223  $property_types = array_keys($default_fields);
224  foreach ($property_types as $property_type) {
225  if (empty($new_fields[$property_type])) {
226  $new_fields[$property_type] = [];
227  }
228  }
229 
230  return $new_fields;
231  };
232 
233  $merge_fields = function ($new_fields) use (&$fields, $clean_field_property_types) {
234  if (empty($new_fields) || !is_array($new_fields)) {
235  return;
236  }
237 
238  $new_fields = $clean_field_property_types($new_fields);
239 
240  $fields = array_merge_recursive($fields, $new_fields);
241  };
242 
243  // normalize type/subtype to support all combinations
244  $normalized_options = $this->normalizeTypeSubtypeOptions($options);
245 
246  $type_subtype_pairs = elgg_extract('type_subtype_pairs', $normalized_options);
247  if (!empty($type_subtype_pairs)) {
248  foreach ($type_subtype_pairs as $entity_type => $entity_subtypes) {
249  $result = $this->hooks->trigger('search:fields', $entity_type, $options, $default_fields);
250  $merge_fields($result);
251 
252  if (elgg_is_empty($entity_subtypes)) {
253  continue;
254  }
255 
256  foreach ($entity_subtypes as $entity_subtype) {
257  $result = $this->hooks->trigger('search:fields', "{$entity_type}:{$entity_subtype}", $options, $default_fields);
258  $merge_fields($result);
259  }
260  }
261  }
262 
263  // search fields for search type
264  $search_type = elgg_extract('search_type', $options, 'entities');
265  if ($search_type) {
266  $fields = $this->hooks->trigger('search:fields', $search_type, $options, $fields);
267  }
268 
269  // make sure all supported field types are available
270  $fields = $clean_field_property_types($fields);
271 
272  if (empty($options['fields'])) {
273  $options['fields'] = $fields;
274  } else {
275  // only allow known fields
276  foreach ($fields as $property_type => $property_type_fields) {
277  if (empty($options['fields'][$property_type])) {
278  $options['fields'][$property_type] = [];
279  continue;
280  }
281 
282  $allowed = array_intersect($property_type_fields, (array) $options['fields'][$property_type]);
283  $options['fields'][$property_type] = array_values(array_unique($allowed));
284  }
285  }
286 
287  return $options;
288  }
289 
297  public function prepareSortOptions(array $options = []) {
298 
299  $sort = elgg_extract('sort', $options);
300  if (is_string($sort)) {
301  $sort = [
302  'property' => $sort,
303  'direction' => elgg_extract('order', $options)
304  ];
305  }
306 
307  if (!isset($sort['property'])) {
308  $sort = [
309  'property' => 'time_created',
310  'property_type' => 'attribute',
311  'direction' => 'desc',
312  ];
313  }
314 
315  $clause = new Database\Clauses\EntitySortByClause();
316  $clause->property = elgg_extract('property', $sort);
317  $clause->property_type = elgg_extract('property_type', $sort);
318  $clause->direction = elgg_extract('direction', $sort, 'asc');
319  $clause->signed = elgg_extract('signed', $sort, false);
320 
321  $options['order_by'] = [$clause];
322 
323  return $options;
324  }
325 
338  public function buildSearchWhereQuery(QueryBuilder $qb, $alias, $fields, $query_parts, $partial_match = true) {
339 
340  $attributes = elgg_extract('attributes', $fields, [], false);
341  $metadata = elgg_extract('metadata', $fields, [], false);
342  $annotations = elgg_extract('annotations', $fields, [], false);
343  $private_settings = elgg_extract('private_settings', $fields, [], false);
344 
345  $ors = [];
346 
347  $populate_where = function ($where, $part) use ($partial_match) {
348  $where->values = $partial_match ? "%{$part}%" : $part;
349  $where->comparison = 'LIKE';
350  $where->value_type = ELGG_VALUE_STRING;
351  $where->case_sensitive = false;
352  };
353 
354  if (!empty($attributes)) {
355  foreach ($attributes as $attribute) {
356  $attribute_ands = [];
357  foreach ($query_parts as $part) {
358  $where = new AttributeWhereClause();
359  $where->names = $attribute;
360  $populate_where($where, $part);
361  $attribute_ands[] = $where->prepare($qb, $alias);
362  }
363  $ors[] = $qb->merge($attribute_ands, 'AND');
364  }
365  }
366 
367  if (!empty($metadata)) {
368  $metadata_ands = [];
369  $md_alias = $qb->joinMetadataTable($alias, 'guid', $metadata, 'left');
370  foreach ($query_parts as $part) {
371  $where = new MetadataWhereClause();
372  $populate_where($where, $part);
373  $metadata_ands[] = $where->prepare($qb, $md_alias);
374  }
375  $ors[] = $qb->merge($metadata_ands, 'AND');
376  }
377 
378  if (!empty($annotations)) {
379  $annotations_ands = [];
380  $an_alias = $qb->joinAnnotationTable($alias, 'guid', $annotations, 'left');
381  foreach ($query_parts as $part) {
382  $where = new AnnotationWhereClause();
383  $populate_where($where, $part);
384  $annotations_ands[] = $where->prepare($qb, $an_alias);
385  }
386  $ors[] = $qb->merge($annotations_ands, 'AND');
387  }
388 
389  if (!empty($private_settings)) {
390  $private_settings_ands = [];
391  $ps_alias = $qb->joinPrivateSettingsTable($alias, 'guid', $private_settings, 'left');
392  foreach ($query_parts as $part) {
393  $where = new PrivateSettingWhereClause();
394  $populate_where($where, $part);
395  $private_settings_ands[] = $where->prepare($qb, $ps_alias);
396  }
397  $ors[] = $qb->merge($private_settings_ands, 'AND');
398  }
399 
400  return $qb->merge($ors, 'OR');
401  }
402 
403 }
$query
Definition: groups.php:8
normalizeOptions(array $options=[])
Normalize options.
static getEntityTypes()
Get the core entity types.
Definition: Config.php:562
__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:206
prepareSearchOptions(array $options=[])
Prepare ege* options.
$metadata
Outputs object metadata $vars[&#39;metadata&#39;] Metadata/menu $vars[&#39;show_entity_menu&#39;] Show the entity m...
Definition: metadata.php:10
elgg_get_entities(array $options=[])
Fetches/counts entities or performs a calculation on their properties.
Definition: entities.php:545
WARNING: API IN FLUX.
search(array $options=[])
Returns search results as an array of entities, as a batch, or a count, depending on parameters given...
$fields
Definition: save.php:28
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:1131
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:139
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.
$attributes
Definition: ajax_loader.php:13
elgg ElggEntity
Definition: ElggEntity.js:15
Builds clauses for filtering entities by properties in metadata table.