Elgg  Version master
MetadataTable.php
Go to the documentation of this file.
1 <?php
2 
3 namespace Elgg\Database;
4 
7 use Elgg\Database;
14 
21 
22  use TimeUsing;
23 
24  protected const MYSQL_TEXT_BYTE_LIMIT = 65535;
25 
26  public const TABLE_NAME = 'metadata';
27 
28  public const DEFAULT_JOIN_ALIAS = 'n_table';
29 
39  public function __construct(
40  protected AccessCache $access_cache,
41  protected MetadataCache $metadata_cache,
42  protected Database $db,
43  protected Events $events,
44  protected EntityTable $entityTable
45  ) {
46  }
47 
62  public function getTags(array $options = []) {
63  $defaults = [
64  'threshold' => 1,
65  'tag_names' => [],
66  ];
67 
68  $options = array_merge($defaults, $options);
69 
70  $singulars = ['tag_name'];
71  $options = QueryOptions::normalizePluralOptions($options, $singulars);
72 
73  $tag_names = elgg_extract('tag_names', $options, ['tags'], false);
74 
75  $threshold = elgg_extract('threshold', $options, 1, false);
76 
77  unset($options['tag_names']);
78  unset($options['threshold']);
79 
80  // custom selects
81  $options['selects'] = [
82  function(QueryBuilder $qb, $main_alias) {
83  return "{$main_alias}.value AS tag";
84  },
85  function(QueryBuilder $qb, $main_alias) {
86  return "COUNT({$main_alias}.id) AS total";
87  },
88  ];
89 
90  // additional wheres
91  $wheres = (array) elgg_extract('wheres', $options, []);
92  $wheres[] = function(QueryBuilder $qb, $main_alias) use ($tag_names) {
93  return $qb->compare("{$main_alias}.name", 'in', $tag_names, ELGG_VALUE_STRING);
94  };
95  $wheres[] = function(QueryBuilder $qb, $main_alias) {
96  return $qb->compare("{$main_alias}.value", '!=', '', ELGG_VALUE_STRING);
97  };
98  $options['wheres'] = $wheres;
99 
100  // custom group by
101  $options['group_by'] = [
102  function(QueryBuilder $qb, $main_alias) {
103  return "{$main_alias}.value";
104  },
105  ];
106 
107  // having
108  $having = (array) elgg_extract('having', $options, []);
109  $having[] = function(QueryBuilder $qb, $main_alias) use ($threshold) {
110  return $qb->compare('total', '>=', $threshold, ELGG_VALUE_INTEGER);
111  };
112  $options['having'] = $having;
113 
114  // order by
115  $options['order_by'] = [
116  new OrderByClause('total', 'desc'),
117  ];
118 
119  // custom callback
120  $options['callback'] = function($row) {
121  $result = new \stdClass();
122  $result->tag = $row->tag;
123  $result->total = (int) $row->total;
124 
125  return $result;
126  };
127 
128  return $this->getAll($options);
129  }
130 
140  public function get(int $id): ?\ElggMetadata {
141  $qb = Select::fromTable(self::TABLE_NAME);
142  $qb->select('*');
143 
144  $where = new MetadataWhereClause();
145  $where->ids = $id;
146  $qb->addClause($where);
147 
148  $row = $this->db->getDataRow($qb);
149  return $row ? new \ElggMetadata($row) : null;
150  }
151 
159  public function delete(\ElggMetadata $metadata): bool {
160  if (!$metadata->id) {
161  return false;
162  }
163 
164  if (!$this->events->trigger('delete', 'metadata', $metadata)) {
165  return false;
166  }
167 
168  $qb = Delete::fromTable(self::TABLE_NAME);
169  $qb->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
170 
171  $deleted = $this->db->deleteData($qb);
172 
173  if ($deleted) {
174  $this->metadata_cache->delete($metadata->entity_guid);
175  }
176 
177  return $deleted !== false;
178  }
179 
192  public function create(\ElggMetadata $metadata, bool $allow_multiple = false): int|false {
193  if (!isset($metadata->value) || !isset($metadata->entity_guid)) {
194  elgg_log('Metadata must have a value and entity guid', \Psr\Log\LogLevel::ERROR);
195  return false;
196  }
197 
198  if (!$this->entityTable->exists($metadata->entity_guid)) {
199  elgg_log("Can't create metadata on a non-existing entity_guid", \Psr\Log\LogLevel::ERROR);
200  return false;
201  }
202 
203  if (!is_scalar($metadata->value)) {
204  elgg_log('To set multiple metadata values use ElggEntity::setMetadata', \Psr\Log\LogLevel::ERROR);
205  return false;
206  }
207 
208  if ($metadata->id) {
209  if ($this->update($metadata)) {
210  return $metadata->id;
211  }
212  }
213 
214  if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
215  elgg_log("Metadata '{$metadata->name}' is above the MySQL TEXT size limit and may be truncated.", \Psr\Log\LogLevel::WARNING);
216  }
217 
218  if (!$allow_multiple) {
219  $id = $this->getIDsByName($metadata->entity_guid, $metadata->name);
220 
221  if (is_array($id)) {
222  throw new LogicException("
223  Multiple '{$metadata->name}' metadata values exist for entity [guid: {$metadata->entity_guid}].
224  Use ElggEntity::setMetadata()
225  ");
226  }
227 
228  if ($id > 0) {
229  $metadata->id = $id;
230 
231  if ($this->update($metadata)) {
232  return $metadata->id;
233  }
234  }
235  }
236 
237  if (!$this->events->triggerBefore('create', 'metadata', $metadata)) {
238  return false;
239  }
240 
241  $time_created = $this->getCurrentTime()->getTimestamp();
242 
243  $qb = Insert::intoTable(self::TABLE_NAME);
244  $qb->values([
245  'name' => $qb->param($metadata->name, ELGG_VALUE_STRING),
246  'entity_guid' => $qb->param($metadata->entity_guid, ELGG_VALUE_INTEGER),
247  'value' => $qb->param($metadata->value, $metadata->value_type === 'text' ? ELGG_VALUE_STRING : ELGG_VALUE_INTEGER),
248  'value_type' => $qb->param($metadata->value_type, ELGG_VALUE_STRING),
249  'time_created' => $qb->param($time_created, ELGG_VALUE_INTEGER),
250  ]);
251 
252  $id = $this->db->insertData($qb);
253 
254  if ($id === 0) {
255  return false;
256  }
257 
258  $metadata->id = (int) $id;
259  $metadata->time_created = $time_created;
260 
261  if (!$this->events->trigger('create', 'metadata', $metadata)) {
262  $this->delete($metadata);
263 
264  return false;
265  }
266 
267  $this->metadata_cache->delete($metadata->entity_guid);
268 
269  $this->events->triggerAfter('create', 'metadata', $metadata);
270 
271  return $id;
272  }
273 
281  public function update(\ElggMetadata $metadata): bool {
282  if (!$this->entityTable->exists($metadata->entity_guid)) {
283  elgg_log("Can't update metadata to a non-existing entity_guid", \Psr\Log\LogLevel::ERROR);
284  return false;
285  }
286 
287  if (!$this->events->triggerBefore('update', 'metadata', $metadata)) {
288  return false;
289  }
290 
291  if (strlen($metadata->value) > self::MYSQL_TEXT_BYTE_LIMIT) {
292  elgg_log("Metadata '{$metadata->name}' is above the MySQL TEXT size limit and may be truncated.", \Psr\Log\LogLevel::WARNING);
293  }
294 
295  $qb = Update::table(self::TABLE_NAME);
296  $qb->set('name', $qb->param($metadata->name, ELGG_VALUE_STRING))
297  ->set('value', $qb->param($metadata->value, $metadata->value_type === 'integer' ? ELGG_VALUE_INTEGER : ELGG_VALUE_STRING))
298  ->set('value_type', $qb->param($metadata->value_type, ELGG_VALUE_STRING))
299  ->where($qb->compare('id', '=', $metadata->id, ELGG_VALUE_INTEGER));
300 
301  $result = $this->db->updateData($qb);
302 
303  if ($result === false) {
304  return false;
305  }
306 
307  $this->metadata_cache->delete($metadata->entity_guid);
308 
309  $this->events->trigger('update', 'metadata', $metadata);
310  $this->events->triggerAfter('update', 'metadata', $metadata);
311 
312  return true;
313  }
314 
326  public function getAll(array $options = []) {
327  $options['metastring_type'] = 'metadata';
328  $options = QueryOptions::normalizeMetastringOptions($options);
329 
330  return Metadata::find($options);
331  }
332 
344  public function getRowsForGuids(array $guids): array {
345  $qb = Select::fromTable(self::TABLE_NAME);
346  $qb->select('*')
347  ->where($qb->compare('entity_guid', 'IN', $guids, ELGG_VALUE_GUID))
348  ->orderBy('entity_guid', 'asc')
349  ->addOrderBy('time_created', 'asc')
350  ->addOrderBy('id', 'asc');
351 
352  return $this->db->getData($qb, function ($row) {
353  return new \ElggMetadata($row);
354  });
355  }
356 
372  public function deleteAll(array $options): bool {
373  $required = [
374  'guid', 'guids',
375  'metadata_name', 'metadata_names',
376  'metadata_value', 'metadata_values',
377  ];
378 
379  $found = false;
380  foreach ($required as $key) {
381  // check that it exists and is something.
382  if (isset($options[$key]) && !elgg_is_empty($options[$key])) {
383  $found = true;
384  break;
385  }
386  }
387 
388  if (!$found) {
389  // requirements not met
390  throw new InvalidArgumentException(__METHOD__ . ' requires at least one of the following keys in $options: ' . implode(', ', $required));
391  }
392 
393  // This moved last in case an object's constructor sets metadata. Currently the batch
394  // delete process has to create the entity to delete its metadata. See #5214
395  if (empty($options['guid'])) {
396  $this->access_cache->clear();
397  $this->metadata_cache->clear();
398  } else {
399  $this->entityTable->invalidateCache($options['guid']);
400  }
401 
402  $options['batch'] = true;
403  $options['batch_size'] = 50;
404  $options['batch_inc_offset'] = false;
405 
406  $metadata = Metadata::find($options);
407  $count = $metadata->count();
408 
409  if (!$count) {
410  return true;
411  }
412 
413  $success = 0;
414  /* @var $md \ElggMetadata */
415  foreach ($metadata as $md) {
416  if ($md->delete()) {
417  $success++;
418  }
419  }
420 
421  return $success === $count;
422  }
423 
432  protected function getIDsByName(int $entity_guid, string $name) {
433  $cached_metadata = $this->metadata_cache->load($entity_guid);
434  if ($cached_metadata !== null) {
435  $ids = [];
436  foreach ($cached_metadata as $md) {
437  if ($md->name !== $name) {
438  continue;
439  }
440 
441  $ids[] = $md->id;
442  }
443  } else {
444  $qb = Select::fromTable(self::TABLE_NAME);
445  $qb->select('id')
446  ->where($qb->compare('entity_guid', '=', $entity_guid, ELGG_VALUE_INTEGER))
447  ->andWhere($qb->compare('name', '=', $name, ELGG_VALUE_STRING));
448 
449  $callback = function (\stdClass $row) {
450  return (int) $row->id;
451  };
452 
453  $ids = $this->db->getData($qb, $callback);
454  }
455 
456  if (empty($ids)) {
457  return null;
458  }
459 
460  if (is_array($ids) && count($ids) === 1) {
461  return array_shift($ids);
462  }
463 
464  return $ids;
465  }
466 }
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.
delete(bool $recursive=true, bool $persistent=null)
Deletes the entity.
foreach($recommendedExtensions as $extension) if(empty(ini_get('session.gc_probability'))||empty(ini_get('session.gc_divisor'))) $db
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.
__construct(protected AccessCache $access_cache, protected MetadataCache $metadata_cache, protected Database $db, protected Events $events, protected EntityTable $entityTable)
Constructor.
$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
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
$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:24
update(\ElggMetadata $metadata)
Update a specific piece of metadata.
getRowsForGuids(array $guids)
Returns metadata rows.