Elgg  Version master
MetadataTable.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
6 use Elgg\Database;
13 
20 
21  use TimeUsing;
22 
23  protected const MYSQL_TEXT_BYTE_LIMIT = 65535;
24 
25  public const TABLE_NAME = 'metadata';
26 
27  public const DEFAULT_JOIN_ALIAS = 'n_table';
28 
30 
31  protected Database $db;
32 
33  protected Events $events;
34 
36 
45  public function __construct(
46  MetadataCache $metadata_cache,
47  Database $db,
48  Events $events,
49  EntityTable $entityTable
50  ) {
51  $this->metadata_cache = $metadata_cache;
52  $this->db = $db;
53  $this->events = $events;
54  $this->entityTable = $entityTable;
55  }
56 
71  public function getTags(array $options = []) {
72  $defaults = [
73  'threshold' => 1,
74  'tag_names' => [],
75  ];
76 
77  $options = array_merge($defaults, $options);
78 
79  $singulars = ['tag_name'];
80  $options = QueryOptions::normalizePluralOptions($options, $singulars);
81 
82  $tag_names = elgg_extract('tag_names', $options, ['tags'], false);
83 
84  $threshold = elgg_extract('threshold', $options, 1, false);
85 
86  unset($options['tag_names']);
87  unset($options['threshold']);
88 
89  // custom selects
90  $options['selects'] = [
91  function(QueryBuilder $qb, $main_alias) {
92  return "{$main_alias}.value AS tag";
93  },
94  function(QueryBuilder $qb, $main_alias) {
95  return "COUNT({$main_alias}.id) AS total";
96  },
97  ];
98 
99  // additional wheres
100  $wheres = (array) elgg_extract('wheres', $options, []);
101  $wheres[] = function(QueryBuilder $qb, $main_alias) use ($tag_names) {
102  return $qb->compare("{$main_alias}.name", 'in', $tag_names, ELGG_VALUE_STRING);
103  };
104  $wheres[] = function(QueryBuilder $qb, $main_alias) {
105  return $qb->compare("{$main_alias}.value", '!=', '', ELGG_VALUE_STRING);
106  };
107  $options['wheres'] = $wheres;
108 
109  // custom group by
110  $options['group_by'] = [
111  function(QueryBuilder $qb, $main_alias) {
112  return "{$main_alias}.value";
113  },
114  ];
115 
116  // having
117  $having = (array) elgg_extract('having', $options, []);
118  $having[] = function(QueryBuilder $qb, $main_alias) use ($threshold) {
119  return $qb->compare('total', '>=', $threshold, ELGG_VALUE_INTEGER);
120  };
121  $options['having'] = $having;
122 
123  // order by
124  $options['order_by'] = [
125  new OrderByClause('total', 'desc'),
126  ];
127 
128  // custom callback
129  $options['callback'] = function($row) {
130  $result = new \stdClass();
131  $result->tag = $row->tag;
132  $result->total = (int) $row->total;
133 
134  return $result;
135  };
136 
137  return $this->getAll($options);
138  }
139 
149  public function get(int $id): ?\ElggMetadata {
150  $qb = Select::fromTable(self::TABLE_NAME);
151  $qb->select('*');
152 
153  $where = new MetadataWhereClause();
154  $where->ids = $id;
155  $qb->addClause($where);
156 
157  $row = $this->db->getDataRow($qb);
158  return $row ? new \ElggMetadata($row) : null;
159  }
160 
168  public function delete(\ElggMetadata $metadata): bool {
169  if (!$metadata->id) {
170  return false;
171  }
172 
173  if (!_elgg_services()->events->trigger('delete', 'metadata', $metadata)) {
174  return false;
175  }
176 
177  $qb = Delete::fromTable(self::TABLE_NAME);
178  $qb->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
179 
180  $deleted = $this->db->deleteData($qb);
181 
182  if ($deleted) {
183  $this->metadata_cache->clear($metadata->entity_guid);
184  }
185 
186  return $deleted !== false;
187  }
188 
201  public function create(\ElggMetadata $metadata, bool $allow_multiple = false): int|false {
202  if (!isset($metadata->value) || !isset($metadata->entity_guid)) {
203  elgg_log('Metadata must have a value and entity guid', 'ERROR');
204  return false;
205  }
206 
207  if (!$this->entityTable->exists($metadata->entity_guid)) {
208  elgg_log("Can't create metadata on a non-existing entity_guid", 'ERROR');
209  return false;
210  }
211 
212  if (!is_scalar($metadata->value)) {
213  elgg_log('To set multiple metadata values use ElggEntity::setMetadata', 'ERROR');
214  return false;
215  }
216 
217  if ($metadata->id) {
218  if ($this->update($metadata)) {
219  return $metadata->id;
220  }
221  }
222 
223  if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
224  elgg_log("Metadata '{$metadata->name}' is above the MySQL TEXT size limit and may be truncated.", 'WARNING');
225  }
226 
227  if (!$allow_multiple) {
228  $id = $this->getIDsByName($metadata->entity_guid, $metadata->name);
229 
230  if (is_array($id)) {
231  throw new LogicException("
232  Multiple '{$metadata->name}' metadata values exist for entity [guid: {$metadata->entity_guid}].
233  Use ElggEntity::setMetadata()
234  ");
235  }
236 
237  if ($id > 0) {
238  $metadata->id = $id;
239 
240  if ($this->update($metadata)) {
241  return $metadata->id;
242  }
243  }
244  }
245 
246  if (!$this->events->triggerBefore('create', 'metadata', $metadata)) {
247  return false;
248  }
249 
250  $time_created = $this->getCurrentTime()->getTimestamp();
251 
252  $qb = Insert::intoTable(self::TABLE_NAME);
253  $qb->values([
254  'name' => $qb->param($metadata->name, ELGG_VALUE_STRING),
255  'entity_guid' => $qb->param($metadata->entity_guid, ELGG_VALUE_INTEGER),
256  'value' => $qb->param($metadata->value, $metadata->value_type === 'text' ? ELGG_VALUE_STRING : ELGG_VALUE_INTEGER),
257  'value_type' => $qb->param($metadata->value_type, ELGG_VALUE_STRING),
258  'time_created' => $qb->param($time_created, ELGG_VALUE_INTEGER),
259  ]);
260 
261  $id = $this->db->insertData($qb);
262 
263  if ($id === false) {
264  return false;
265  }
266 
267  $metadata->id = (int) $id;
268  $metadata->time_created = $time_created;
269 
270  if (!$this->events->trigger('create', 'metadata', $metadata)) {
271  $this->delete($metadata);
272 
273  return false;
274  }
275 
276  $this->metadata_cache->clear($metadata->entity_guid);
277 
278  $this->events->triggerAfter('create', 'metadata', $metadata);
279 
280  return $id;
281  }
282 
290  public function update(\ElggMetadata $metadata): bool {
291  if (!$this->entityTable->exists($metadata->entity_guid)) {
292  elgg_log("Can't update metadata to a non-existing entity_guid", 'ERROR');
293  return false;
294  }
295 
296  if (!$this->events->triggerBefore('update', 'metadata', $metadata)) {
297  return false;
298  }
299 
300  if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
301  elgg_log("Metadata '{$metadata->name}' is above the MySQL TEXT size limit and may be truncated.", 'WARNING');
302  }
303 
304  $qb = Update::table(self::TABLE_NAME);
305  $qb->set('name', $qb->param($metadata->name, ELGG_VALUE_STRING))
306  ->set('value', $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING))
307  ->set('value_type', $qb->param($metadata->value_type, ELGG_VALUE_STRING))
308  ->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
309 
310  $result = $this->db->updateData($qb);
311 
312  if ($result === false) {
313  return false;
314  }
315 
316  $this->metadata_cache->clear($metadata->entity_guid);
317 
318  $this->events->trigger('update', 'metadata', $metadata);
319  $this->events->triggerAfter('update', 'metadata', $metadata);
320 
321  return true;
322  }
323 
335  public function getAll(array $options = []) {
336  $options['metastring_type'] = 'metadata';
337  $options = QueryOptions::normalizeMetastringOptions($options);
338 
339  return Metadata::find($options);
340  }
341 
353  public function getRowsForGuids(array $guids): array {
354  $qb = Select::fromTable(self::TABLE_NAME);
355  $qb->select('*')
356  ->where($qb->compare('entity_guid', 'IN', $guids, ELGG_VALUE_GUID))
357  ->orderBy('entity_guid', 'asc')
358  ->addOrderBy('time_created', 'asc')
359  ->addOrderBy('id', 'asc');
360 
361  return $this->db->getData($qb, function ($row) {
362  return new \ElggMetadata($row);
363  });
364  }
365 
381  public function deleteAll(array $options): bool {
382  $required = [
383  'guid', 'guids',
384  'metadata_name', 'metadata_names',
385  'metadata_value', 'metadata_values',
386  ];
387 
388  $found = false;
389  foreach ($required as $key) {
390  // check that it exists and is something.
391  if (isset($options[$key]) && !elgg_is_empty($options[$key])) {
392  $found = true;
393  break;
394  }
395  }
396 
397  if (!$found) {
398  // requirements not met
399  throw new InvalidArgumentException(__METHOD__ . ' requires at least one of the following keys in $options: ' . implode(', ', $required));
400  }
401 
402  // This moved last in case an object's constructor sets metadata. Currently the batch
403  // delete process has to create the entity to delete its metadata. See #5214
404  $this->metadata_cache->invalidateByOptions($options);
405 
406  $options['batch'] = true;
407  $options['batch_size'] = 50;
408  $options['batch_inc_offset'] = false;
409 
410  $metadata = Metadata::find($options);
411  $count = $metadata->count();
412 
413  if (!$count) {
414  return true;
415  }
416 
417  $success = 0;
418  /* @var $md \ElggMetadata */
419  foreach ($metadata as $md) {
420  if ($md->delete()) {
421  $success++;
422  }
423  }
424 
425  return $success === $count;
426  }
427 
436  protected function getIDsByName(int $entity_guid, string $name) {
437  if ($this->metadata_cache->isLoaded($entity_guid)) {
438  $ids = $this->metadata_cache->getSingleId($entity_guid, $name);
439  } else {
440  $qb = Select::fromTable(self::TABLE_NAME);
441  $qb->select('id')
442  ->where($qb->compare('entity_guid', '=', $entity_guid, ELGG_VALUE_INTEGER))
443  ->andWhere($qb->compare('name', '=', $name, ELGG_VALUE_STRING));
444 
445  $callback = function (\stdClass $row) {
446  return (int) $row->id;
447  };
448 
449  $ids = $this->db->getData($qb, $callback);
450  }
451 
452  if (empty($ids)) {
453  return null;
454  }
455 
456  if (is_array($ids) && count($ids) === 1) {
457  return array_shift($ids);
458  }
459 
460  return $ids;
461  }
462 }
static table(string $table)
Returns a QueryBuilder for updating data in a given table.
Definition: Update.php:17
$deleted
Definition: delete.php:25
Exception thrown if an argument is not of the expected type.
create(\ElggMetadata $metadata, bool $allow_multiple=false)
Create a new metadata object, or update an existing one (if multiple is allowed)
$defaults
Generic entity header upload helper.
Definition: header.php:6
static find(array $options=[])
Build and execute a new query from an array of legacy options.
Definition: Repository.php:110
getAll(array $options=[])
Returns metadata.
if(!$user||!$user->canDelete()) $name
Definition: delete.php:22
The Elgg database.
Definition: Database.php:26
if(!$user instanceof\ElggUser) $time_created
Definition: online.php:13
const ELGG_VALUE_INTEGER
Value types.
Definition: constants.php:111
const ELGG_VALUE_GUID
Definition: constants.php:113
Database abstraction query builder.
trait TimeUsing
Adds methods for setting the current time (for testing)
Definition: TimeUsing.php:10
static intoTable(string $table)
Returns a QueryBuilder for inserting data in a given table.
Definition: Insert.php:17
elgg_is_empty($value)
Check if a value isn&#39;t empty, but allow 0 and &#39;0&#39;.
Definition: input.php:176
getIDsByName(int $entity_guid, string $name)
Returns ID(s) of metadata with a particular name attached to an entity.
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
$entity_guid
Action for adding and editing comments.
Definition: save.php:6
getCurrentTime($modifier= '')
Get the (cloned) time.
Definition: TimeUsing.php:25
if($who_can_change_language=== 'nobody') elseif($who_can_change_language=== 'admin_only'&&!elgg_is_admin_logged_in()) $options
Definition: language.php:20
deleteAll(array $options)
Deletes metadata based on $options.
elgg_log($message, $level=\Psr\Log\LogLevel::NOTICE)
Log a message.
Definition: elgglib.php:86
compare(string $x, string $comparison, $y=null, string $type=null, bool $case_sensitive=null)
Build value comparison clause.
$count
Definition: ban.php:24
ElggMetadata.
static fromTable(string $table)
Returns a QueryBuilder for deleting data from a given table.
Definition: Delete.php:17
Exception that represents error in the program logic.
Extends QueryBuilder with ORDER BY clauses.
if($container instanceof ElggGroup &&$container->guid!=elgg_get_page_owner_guid()) $key
Definition: summary.php:44
getTags(array $options=[])
Get popular tags and their frequencies.
$guids
Activates all specified installed and inactive plugins.
Definition: activate_all.php:9
__construct(MetadataCache $metadata_cache, Database $db, Events $events, EntityTable $entityTable)
Constructor.
const ELGG_VALUE_STRING
Definition: constants.php:112
$metadata
Output annotation metadata.
Definition: metadata.php:9
In memory cache of known metadata values stored by entity.
$required
Definition: label.php:12
static fromTable(string $table, string $alias=null)
Returns a QueryBuilder for selecting data from a given table.
Definition: Select.php:18
_elgg_services()
Get the global service provider.
Definition: elgglib.php:351
$id
Generic annotation delete action.
Definition: delete.php:6
$qb
Definition: queue.php:12
Builds clauses for filtering entities by properties in metadata table.
This class interfaces with the database to perform CRUD operations on metadata.
Entity table database service.
Definition: EntityTable.php:25
update(\ElggMetadata $metadata)
Update a specific piece of metadata.
getRowsForGuids(array $guids)
Returns metadata rows.