ElggEntity::create()   F
last analyzed

Complexity

Conditions 23
Paths 194

Size

Total Lines 136
Code Lines 80

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 64
CRAP Score 27.892

Importance

Changes 0
Metric Value
cc 23
eloc 80
nc 194
nop 0
dl 0
loc 136
ccs 64
cts 81
cp 0.7901
crap 27.892
rs 3.3833
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
use Elgg\Database\QueryBuilder;
4
use Elgg\Exceptions\DatabaseException;
5
use Elgg\Exceptions\Filesystem\IOException;
6
use Elgg\Exceptions\DomainException as ElggDomainException;
7
use Elgg\Exceptions\InvalidArgumentException as ElggInvalidArgumentException;
8
use Elgg\Traits\Entity\AccessCollections;
9
use Elgg\Traits\Entity\Annotations;
10
use Elgg\Traits\Entity\Icons;
11
use Elgg\Traits\Entity\Metadata;
12
use Elgg\Traits\Entity\Relationships;
13
use Elgg\Traits\Entity\Subscriptions;
14
15
/**
16
 * The parent class for all Elgg Entities.
17
 *
18
 * An \ElggEntity is one of the basic data models in Elgg.
19
 * It is the primary means of storing and retrieving data from the database.
20
 * An \ElggEntity represents one row of the entities table.
21
 *
22
 * The \ElggEntity class handles CRUD operations for the entities table.
23
 * \ElggEntity should always be extended by another class to handle CRUD
24
 * operations on the type-specific table.
25
 *
26
 * \ElggEntity uses magic methods for get and set, so any property that isn't
27
 * declared will be assumed to be metadata and written to the database
28
 * as metadata on the object.  All children classes must declare which
29
 * properties are columns of the type table or they will be assumed
30
 * to be metadata.  See \ElggObject::initializeAttributes() for examples.
31
 *
32
 * Core supports 4 types of entities: \ElggObject, \ElggUser, \ElggGroup, and \ElggSite.
33
 *
34
 * @tip Plugin authors will want to extend the \ElggObject class, not this class.
35
 *
36
 * @property-read  string $type           object, user, group, or site (read-only after save)
37
 * @property-read  string $subtype        Further clarifies the nature of the entity
38
 * @property-read  int    $guid           The unique identifier for this entity (read only)
39
 * @property       int    $owner_guid     The GUID of the owner of this entity (usually the creator)
40
 * @property       int    $container_guid The GUID of the entity containing this entity
41
 * @property       int    $access_id      Specifies the visibility level of this entity
42
 * @property       int    $time_created   A UNIX timestamp of when the entity was created
43
 * @property-read  int    $time_updated   A UNIX timestamp of when the entity was last updated (automatically updated on save)
44
 * @property-read  int    $last_action    A UNIX timestamp of when the entity was last acted upon
45
 * @property-read  int    $time_deleted   A UNIX timestamp of when the entity was deleted
46
 * @property-read  string $deleted        Is this entity deleted ('yes' or 'no')
47
 * @property-read  string $enabled        Is this entity enabled ('yes' or 'no')
48
 *
49
 * Metadata (the above are attributes)
50
 * @property       string $location       A location of the entity
51
 */
52
abstract class ElggEntity extends \ElggData {
53
54
	use AccessCollections;
55
	use Annotations;
56
	use Icons;
57
	use Metadata;
58
	use Relationships;
59
	use Subscriptions;
60
	
61
	public const PRIMARY_ATTR_NAMES = [
62
		'guid',
63
		'type',
64
		'subtype',
65
		'owner_guid',
66
		'container_guid',
67
		'access_id',
68
		'time_created',
69
		'time_updated',
70
		'last_action',
71
		'enabled',
72
		'deleted',
73
		'time_deleted',
74
	];
75
76
	/**
77
	 * @var string[] attributes that are integers
78
	 */
79
	protected const INTEGER_ATTR_NAMES = [
80
		'guid',
81
		'owner_guid',
82
		'container_guid',
83
		'access_id',
84
		'time_created',
85
		'time_updated',
86
		'last_action',
87
		'time_deleted',
88
	];
89
90
	/**
91
	 * Volatile data structure for this object, allows for storage of data
92
	 * in-memory that isn't synced back to the metadata table.
93
	 * @var array
94
	 */
95
	protected $volatile = [];
96
97
	/**
98
	 * Holds the original (persisted) attribute values that have been changed but not yet saved.
99
	 * @var array
100
	 */
101
	protected $orig_attributes = [];
102
103
	/**
104
	 * @var bool
105
	 */
106
	protected $_is_cacheable = true;
107
108
	/**
109
	 * Create a new entity.
110
	 *
111
	 * Plugin developers should only use the constructor to create a new entity.
112
	 * To retrieve entities, use get_entity() and the elgg_get_entities* functions.
113
	 *
114
	 * If no arguments are passed, it creates a new entity.
115
	 * If a database result is passed as a \stdClass instance, it instantiates
116
	 * that entity.
117
	 *
118
	 * @param null|\stdClass $row Database row result. Default is null to create a new object.
119
	 *
120
	 * @throws IOException If cannot load remaining data from db
121
	 */
122 8644
	public function __construct(?\stdClass $row = null) {
123 8644
		$this->initializeAttributes();
124
125 8644
		if (!empty($row) && !$this->load($row)) {
126
			throw new IOException('Failed to load new ' . get_class() . " for GUID: {$row->guid}");
127
		}
128
	}
129
130
	/**
131
	 * Initialize the attributes array.
132
	 *
133
	 * This is vital to distinguish between metadata and base parameters.
134
	 *
135
	 * @return void
136
	 */
137 8644
	protected function initializeAttributes() {
138 8644
		parent::initializeAttributes();
139
140 8644
		$this->attributes['guid'] = null;
141 8644
		$this->attributes['type'] = null;
142 8644
		$this->attributes['subtype'] = null;
143
144 8644
		$this->attributes['owner_guid'] = _elgg_services()->session_manager->getLoggedInUserGuid();
145 8644
		$this->attributes['container_guid'] = _elgg_services()->session_manager->getLoggedInUserGuid();
146
147 8644
		$this->attributes['access_id'] = ACCESS_PRIVATE;
148 8644
		$this->attributes['time_updated'] = null;
149 8644
		$this->attributes['last_action'] = null;
150 8644
		$this->attributes['enabled'] = 'yes';
151 8644
		$this->attributes['deleted'] = 'no';
152 8644
		$this->attributes['time_deleted'] = null;
153
	}
154
155
	/**
156
	 * Clone an entity
157
	 *
158
	 * Resets the guid so that the entity can be saved as a distinct entity from
159
	 * the original. Creation time will be set when this new entity is saved.
160
	 * The owner and container guids come from the original entity. The clone
161
	 * method copies metadata but does not copy annotations.
162
	 *
163
	 * @return void
164
	 */
165 3
	public function __clone() {
166 3
		$orig_entity = get_entity($this->guid);
167 3
		if (!$orig_entity) {
168
			_elgg_services()->logger->error("Failed to clone entity with GUID $this->guid");
169
			return;
170
		}
171
172 3
		$metadata_array = elgg_get_metadata([
173 3
			'guid' => $this->guid,
174 3
			'limit' => false,
175 3
		]);
176
177 3
		$this->attributes['guid'] = null;
178 3
		$this->attributes['time_created'] = null;
179 3
		$this->attributes['time_updated'] = null;
180 3
		$this->attributes['last_action'] = null;
181
182 3
		$this->attributes['subtype'] = $orig_entity->getSubtype();
183
184
		// copy metadata over to new entity - slightly convoluted due to
185
		// handling of metadata arrays
186 3
		if (is_array($metadata_array)) {
187
			// create list of metadata names
188 3
			$metadata_names = [];
189 3
			foreach ($metadata_array as $metadata) {
190 3
				$metadata_names[] = $metadata->name;
191
			}
192
			
193
			// arrays are stored with multiple entries per name
194 3
			$metadata_names = array_unique($metadata_names);
195
196
			// move the metadata over
197 3
			foreach ($metadata_names as $name) {
198 3
				$this->__set($name, $orig_entity->$name);
199
			}
200
		}
201
	}
202
203
	/**
204
	 * Set an attribute or metadata value for this entity
205
	 *
206
	 * Anything that is not an attribute is saved as metadata.
207
	 *
208
	 * Be advised that metadata values are cast to integer or string.
209
	 * You can save booleans, but they will be stored and returned as integers.
210
	 *
211
	 * @param string $name  Name of the attribute or metadata
212
	 * @param mixed  $value The value to be set
213
	 *
214
	 * @return void
215
	 * @throws \Elgg\Exceptions\InvalidArgumentException
216
	 * @see \ElggEntity::setMetadata()
217
	 */
218 2173
	public function __set($name, $value) {
219 2173
		if ($this->$name === $value) {
220
			// quick return if value is not changing
221 1503
			return;
222
		}
223
224 2172
		if (array_key_exists($name, $this->attributes)) {
225
			// if an attribute is 1 (integer) and it's set to "1" (string), don't consider that a change.
226 1277
			if (is_int($this->attributes[$name])
227 1277
					&& is_string($value)
228 1277
					&& ((string) $this->attributes[$name] === $value)) {
229 1
				return;
230
			}
231
232
			// keep original values
233 1277
			if ($this->guid && !array_key_exists($name, $this->orig_attributes)) {
234 359
				$this->orig_attributes[$name] = $this->attributes[$name];
235
			}
236
237
			// Certain properties should not be manually changed!
238
			switch ($name) {
239 1277
				case 'guid':
240 1275
				case 'last_action':
241 1273
				case 'time_deleted':
242 1273
				case 'time_updated':
243 1271
				case 'type':
244 9
					return;
245 1269
				case 'subtype':
246 2
					throw new ElggInvalidArgumentException(elgg_echo('ElggEntity:Error:SetSubtype', ['setSubtype()']));
247 1267
				case 'enabled':
248 2
					throw new ElggInvalidArgumentException(elgg_echo('ElggEntity:Error:SetEnabled', ['enable() / disable()']));
249 1265
				case 'deleted':
250 2
					throw new ElggInvalidArgumentException(elgg_echo('ElggEntity:Error:SetDeleted', ['delete() / restore()']));
251 1263
				case 'access_id':
252 1248
				case 'owner_guid':
253 1053
				case 'container_guid':
254 1161
					if ($value !== null) {
255 1157
						$this->attributes[$name] = (int) $value;
256
					} else {
257 7
						$this->attributes[$name] = null;
258
					}
259 1161
					break;
260
				default:
261 1029
					$this->attributes[$name] = $value;
262 1029
					break;
263
			}
264
			
265 1263
			return;
266
		}
267
268 2147
		$this->setMetadata($name, $value);
269
	}
270
271
	/**
272
	 * Get the original values of attribute(s) that have been modified since the entity was persisted.
273
	 *
274
	 * @return array
275
	 */
276 1158
	public function getOriginalAttributes(): array {
277 1158
		return $this->orig_attributes;
278
	}
279
280
	/**
281
	 * Get an attribute or metadata value
282
	 *
283
	 * If the name matches an attribute, the attribute is returned. If metadata
284
	 * does not exist with that name, a null is returned.
285
	 *
286
	 * This only returns an array if there are multiple values for a particular
287
	 * $name key.
288
	 *
289
	 * @param string $name Name of the attribute or metadata
290
	 *
291
	 * @return mixed
292
	 */
293 4495
	public function __get($name) {
294 4495
		if (array_key_exists($name, $this->attributes)) {
295 2252
			return $this->attributes[$name];
296
		}
297
298 4405
		return $this->getMetadata($name);
299
	}
300
301
	/**
302
	 * Get the entity's display name
303
	 *
304
	 * @return string The title or name of this entity.
305
	 */
306 1416
	public function getDisplayName(): string {
307 1416
		return (string) $this->name;
308
	}
309
310
	/**
311
	 * Sets the title or name of this entity.
312
	 *
313
	 * @param string $display_name The title or name of this entity.
314
	 *
315
	 * @return void
316
	 */
317
	public function setDisplayName(string $display_name): void {
318
		$this->name = $display_name;
319
	}
320
321
	/**
322
	 * Get a piece of volatile (non-persisted) data on this entity.
323
	 *
324
	 * @param string $name The name of the volatile data
325
	 *
326
	 * @return mixed The value or null if not found.
327
	 */
328 3020
	public function getVolatileData(string $name) {
329 3020
		return array_key_exists($name, $this->volatile) ? $this->volatile[$name] : null;
330
	}
331
332
	/**
333
	 * Set a piece of volatile (non-persisted) data on this entity
334
	 *
335
	 * @param string $name  Name
336
	 * @param mixed  $value Value
337
	 *
338
	 * @return void
339
	 */
340 3017
	public function setVolatileData(string $name, $value): void {
341 3017
		$this->volatile[$name] = $value;
342
	}
343
344
	/**
345
	 * Removes all river items related to this entity
346
	 *
347
	 * @return void
348
	 */
349 1471
	public function removeAllRelatedRiverItems(): void {
350 1471
		elgg_delete_river(['subject_guid' => $this->guid, 'limit' => false]);
351 1471
		elgg_delete_river(['object_guid' => $this->guid, 'limit' => false]);
352 1471
		elgg_delete_river(['target_guid' => $this->guid, 'limit' => false]);
353
	}
354
355
	/**
356
	 * Count the number of comments attached to this entity.
357
	 *
358
	 * @return int Number of comments
359
	 * @since 1.8.0
360
	 */
361 6
	public function countComments(): int {
362 6
		if (!$this->hasCapability('commentable')) {
363
			return 0;
364
		}
365
		
366 6
		$params = ['entity' => $this];
367 6
		$num = _elgg_services()->events->triggerResults('comments:count', $this->getType(), $params);
368
369 6
		if (is_int($num)) {
370
			return $num;
371
		}
372
		
373 6
		return \Elgg\Comments\DataService::instance()->getCommentsCount($this);
374
	}
375
376
	/**
377
	 * Check if the given user has access to this entity
378
	 *
379
	 * @param int $user_guid the GUID of the user to check access for (default: logged in user_guid)
380
	 *
381
	 * @return bool
382
	 * @since 4.3
383
	 */
384 97
	public function hasAccess(int $user_guid = 0): bool {
385 97
		return _elgg_services()->accessCollections->hasAccessToEntity($this, $user_guid);
386
	}
387
388
	/**
389
	 * Can a user edit this entity?
390
	 *
391
	 * @tip Can be overridden by registering for the permissions_check event.
392
	 *
393
	 * @param int $user_guid The user GUID, optionally (default: logged in user)
394
	 *
395
	 * @return bool Whether this entity is editable by the given user.
396
	 */
397 1759
	public function canEdit(int $user_guid = 0): bool {
398 1759
		return _elgg_services()->userCapabilities->canEdit($this, $user_guid);
399
	}
400
401
	/**
402
	 * Can a user delete this entity?
403
	 *
404
	 * @tip Can be overridden by registering for the permissions_check:delete event.
405
	 *
406
	 * @param int $user_guid The user GUID, optionally (default: logged in user)
407
	 *
408
	 * @return bool Whether this entity is deletable by the given user.
409
	 * @since 1.11
410
	 */
411 1715
	public function canDelete(int $user_guid = 0): bool {
412 1715
		return _elgg_services()->userCapabilities->canDelete($this, $user_guid);
413
	}
414
415
	/**
416
	 * Can a user add an entity to this container
417
	 *
418
	 * @param int    $user_guid The GUID of the user creating the entity (0 for logged in user).
419
	 * @param string $type      The type of entity we're looking to write
420
	 * @param string $subtype   The subtype of the entity we're looking to write
421
	 *
422
	 * @return bool
423
	 * @throws \Elgg\Exceptions\InvalidArgumentException
424
	 */
425 1078
	public function canWriteToContainer(int $user_guid = 0, string $type = '', string $subtype = ''): bool {
426 1078
		if (empty($type) || empty($subtype)) {
427
			throw new ElggInvalidArgumentException(__METHOD__ . ' requires $type and $subtype to be set');
428
		}
429
		
430 1078
		return _elgg_services()->userCapabilities->canWriteToContainer($this, $type, $subtype, $user_guid);
431
	}
432
433
	/**
434
	 * Can a user comment on an entity?
435
	 *
436
	 * @tip Can be overridden by registering for the 'permissions_check:comment', '<entity type>' event.
437
	 *
438
	 * @param int $user_guid User guid (default is logged in user)
439
	 *
440
	 * @return bool
441
	 */
442 24
	public function canComment(int $user_guid = 0): bool {
443 24
		return _elgg_services()->userCapabilities->canComment($this, $user_guid);
444
	}
445
446
	/**
447
	 * Can a user annotate an entity?
448
	 *
449
	 * @tip Can be overridden by registering for the event [permissions_check:annotate:<name>,
450
	 * <entity type>] or [permissions_check:annotate, <entity type>]. The events are called in that order.
451
	 *
452
	 * @tip If you want logged out users to annotate an object, do not call
453
	 * canAnnotate(). It's easier than using the event.
454
	 *
455
	 * @param int    $user_guid       User guid (default is logged in user)
456
	 * @param string $annotation_name The name of the annotation (default is unspecified)
457
	 *
458
	 * @return bool
459
	 */
460 15
	public function canAnnotate(int $user_guid = 0, string $annotation_name = ''): bool {
461 15
		return _elgg_services()->userCapabilities->canAnnotate($this, $user_guid, $annotation_name);
462
	}
463
464
	/**
465
	 * Returns the guid.
466
	 *
467
	 * @return int|null GUID
468
	 */
469 24
	public function getGUID(): ?int {
470 24
		return $this->guid;
471
	}
472
473
	/**
474
	 * Returns the entity type
475
	 *
476
	 * @return string The entity type
477
	 */
478 1827
	public function getType(): string {
479 1827
		return (string) $this->attributes['type'];
480
	}
481
482
	/**
483
	 * Set the subtype of the entity
484
	 *
485
	 * @param string $subtype the new type
486
	 *
487
	 * @return void
488
	 * @see self::initializeAttributes()
489
	 */
490 1446
	public function setSubtype(string $subtype): void {
491
		// keep original values
492 1446
		if ($this->guid && !array_key_exists('subtype', $this->orig_attributes)) {
493 3
			$this->orig_attributes['subtype'] = $this->attributes['subtype'];
494
		}
495
		
496 1446
		$this->attributes['subtype'] = $subtype;
497
	}
498
499
	/**
500
	 * Get the entity subtype
501
	 *
502
	 * @return string The entity subtype
503
	 */
504 1562
	public function getSubtype(): string {
505 1562
		return (string) $this->attributes['subtype'];
506
	}
507
508
	/**
509
	 * Get the guid of the entity's owner.
510
	 *
511
	 * @return int The owner GUID
512
	 */
513 76
	public function getOwnerGUID(): int {
514 76
		return (int) $this->owner_guid;
515
	}
516
517
	/**
518
	 * Gets the \ElggEntity that owns this entity.
519
	 *
520
	 * @return \ElggEntity|null
521
	 */
522 1441
	public function getOwnerEntity(): ?\ElggEntity {
523 1441
		return $this->owner_guid ? get_entity($this->owner_guid) : null;
524
	}
525
526
	/**
527
	 * Set the container for this object.
528
	 *
529
	 * @param int $container_guid The ID of the container.
530
	 *
531
	 * @return void
532
	 */
533 1
	public function setContainerGUID(int $container_guid): void {
534 1
		$this->container_guid = $container_guid;
535
	}
536
537
	/**
538
	 * Gets the container GUID for this entity.
539
	 *
540
	 * @return int
541
	 */
542 1076
	public function getContainerGUID(): int {
543 1076
		return (int) $this->container_guid;
544
	}
545
546
	/**
547
	 * Get the container entity for this object.
548
	 *
549
	 * @return \ElggEntity|null
550
	 * @since 1.8.0
551
	 */
552 1050
	public function getContainerEntity(): ?\ElggEntity {
553 1050
		return $this->container_guid ? get_entity($this->getContainerGUID()) : null;
554
	}
555
556
	/**
557
	 * Returns the UNIX epoch time that this entity was last updated
558
	 *
559
	 * @return int UNIX epoch time
560
	 */
561 72
	public function getTimeUpdated(): int {
562 72
		return (int) $this->time_updated;
563
	}
564
565
	/**
566
	 * Gets the URL for this entity.
567
	 *
568
	 * Plugins can register for the 'entity:url', '<type>' event to
569
	 * customize the url for an entity.
570
	 *
571
	 * @return string The URL of the entity
572
	 */
573 169
	public function getURL(): string {
574 169
		$url = elgg_generate_entity_url($this, 'view');
575
576 169
		$url = _elgg_services()->events->triggerResults('entity:url', "{$this->getType()}:{$this->getSubtype()}", ['entity' => $this], $url);
577 169
		$url = _elgg_services()->events->triggerResults('entity:url', $this->getType(), ['entity' => $this], $url);
578 169
		if (empty($url)) {
579 58
			return '';
580
		}
581
582 134
		return elgg_normalize_url($url);
583
	}
584
585
	/**
586
	 * {@inheritDoc}
587
	 */
588 1501
	public function save(): bool {
589 1501
		if ($this->guid > 0) {
590 1383
			$result = $this->update();
591
		} else {
592 1497
			$result = $this->create() !== false;
593
		}
594
595 1501
		if ($result) {
596 1500
			$this->cache();
597
		}
598
599 1501
		return $result;
600
	}
601
602
	/**
603
	 * Create a new entry in the entities table.
604
	 *
605
	 * Saves the base information in the entities table for the entity.  Saving
606
	 * the type-specific information is handled in the calling class method.
607
	 *
608
	 * @return int|false The new entity's GUID or false if prevented by an event handler
609
	 *
610
	 * @throws \Elgg\Exceptions\DomainException If the entity's type has not been set.
611
	 * @throws \Elgg\Exceptions\InvalidArgumentException If the entity's subtype has not been set, access_id is invalid or something is wrong with the owner or container
612
	 * @throws \Elgg\Exceptions\Filesystem\IOException If the new row fails to write to the DB.
613
	 */
614 1497
	protected function create() {
615
616 1497
		$type = $this->attributes['type'];
617 1497
		if (!in_array($type, \Elgg\Config::ENTITY_TYPES)) {
618
			throw new ElggDomainException('Entity type must be one of the allowed types: ' . implode(', ', \Elgg\Config::ENTITY_TYPES));
619
		}
620
621 1497
		$subtype = $this->attributes['subtype'];
622 1497
		if (!$subtype) {
623
			throw new ElggInvalidArgumentException('All entities must have a subtype');
624
		}
625
626 1497
		$owner_guid = (int) $this->attributes['owner_guid'];
627 1497
		$access_id = (int) $this->attributes['access_id'];
628 1497
		$now = $this->getCurrentTime()->getTimestamp();
629 1497
		$time_created = isset($this->attributes['time_created']) ? (int) $this->attributes['time_created'] : $now;
630 1497
		$deleted = $this->attributes['deleted'];
631 1497
		$time_deleted = (int) $this->attributes['time_deleted'];
632
633 1497
		$container_guid = $this->attributes['container_guid'];
634 1497
		if ($container_guid == 0) {
635 1379
			$container_guid = $owner_guid;
636 1379
			$this->attributes['container_guid'] = $container_guid;
637
		}
638
		
639 1497
		$container_guid = (int) $container_guid;
640
641 1497
		if ($access_id === ACCESS_DEFAULT) {
642
			throw new ElggInvalidArgumentException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
643
		}
644
		
645 1497
		if ($access_id === ACCESS_FRIENDS) {
646
			throw new ElggInvalidArgumentException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
647
		}
648
649 1497
		$user_guid = _elgg_services()->session_manager->getLoggedInUserGuid();
650
651
		// If given an owner, verify it can be loaded
652 1497
		if (!empty($owner_guid)) {
653 1047
			$owner = $this->getOwnerEntity();
654 1047
			if (!$owner instanceof \ElggEntity) {
655
				$error = "User {$user_guid} tried to create a ({$type}, {$subtype}),";
656
				$error .= " but the given owner {$owner_guid} could not be loaded.";
657
				throw new ElggInvalidArgumentException($error);
658
			}
659
660
			// If different owner than logged in, verify can write to container.
661 1047
			if ($user_guid !== $owner_guid && !$owner->canEdit() && !$owner->canWriteToContainer($user_guid, $type, $subtype)) {
662
				$error = "User {$user_guid} tried to create a ({$type}, {$subtype}) with owner {$owner_guid},";
663
				$error .= " but the user wasn't permitted to write to the owner's container.";
664
				throw new ElggInvalidArgumentException($error);
665
			}
666
		}
667
668
		// If given a container, verify it can be loaded and that the current user can write to it
669 1497
		if (!empty($container_guid)) {
670 1047
			$container = $this->getContainerEntity();
671 1047
			if (!$container instanceof \ElggEntity) {
672
				$error = "User {$user_guid} tried to create a ({$type}, {$subtype}),";
673
				$error .= " but the given container {$container_guid} could not be loaded.";
674
				throw new ElggInvalidArgumentException($error);
675
			}
676
677 1047
			if (!$container->canWriteToContainer($user_guid, $type, $subtype)) {
678
				$error = "User {$user_guid} tried to create a ({$type}, {$subtype}),";
679
				$error .= " but was not permitted to write to container {$container_guid}.";
680
				throw new ElggInvalidArgumentException($error);
681
			}
682
		}
683
		
684 1497
		if (!_elgg_services()->events->triggerBefore('create', $this->type, $this)) {
685 1
			return false;
686
		}
687
688
		// Create primary table row
689 1497
		$guid = _elgg_services()->entityTable->insertRow((object) [
690 1497
			'type' => $type,
691 1497
			'subtype' => $subtype,
692 1497
			'owner_guid' => $owner_guid,
693 1497
			'container_guid' => $container_guid,
694 1497
			'access_id' => $access_id,
695 1497
			'time_created' => $time_created,
696 1497
			'time_updated' => $now,
697 1497
			'last_action' => $now,
698 1497
			'deleted' => $deleted,
699 1497
			'time_deleted' => $time_deleted
700 1497
		], $this->attributes);
701
702 1497
		if (!$guid) {
703
			throw new IOException("Unable to save new object's base entity information!");
704
		}
705
706 1497
		$this->attributes['subtype'] = $subtype;
707 1497
		$this->attributes['guid'] = (int) $guid;
708 1497
		$this->attributes['time_created'] = (int) $time_created;
709 1497
		$this->attributes['time_updated'] = (int) $now;
710 1497
		$this->attributes['last_action'] = (int) $now;
711 1497
		$this->attributes['container_guid'] = (int) $container_guid;
712 1497
		$this->attributes['deleted'] = $deleted;
713 1497
		$this->attributes['time_deleted'] = (int) $time_deleted;
714
715
		// We are writing this new entity to cache to make sure subsequent calls
716
		// to get_entity() load the entity from cache and not from the DB. This
717
		// MUST come before the metadata and annotation writes below!
718 1497
		$this->cache();
719
720
		// Save any unsaved metadata
721 1497
		if (count($this->temp_metadata) > 0) {
722 1495
			foreach ($this->temp_metadata as $name => $value) {
723
				// temp metadata is always an array, but if there is only one value return just the value
724 1495
				$this->setMetadata($name, $value, '', count($value) > 1);
725
			}
726
727 1495
			$this->temp_metadata = [];
728
		}
729
730
		// Save any unsaved annotations.
731 1497
		if (count($this->temp_annotations) > 0) {
732 3
			foreach ($this->temp_annotations as $name => $value) {
733 3
				$this->annotate($name, $value);
734
			}
735
736 3
			$this->temp_annotations = [];
737
		}
738
		
739 1497
		if (isset($container) && !$container instanceof \ElggUser) {
740
			// users have their own logic for setting last action
741 102
			$container->updateLastAction();
742
		}
743
		
744
		// for BC reasons this event is still needed (for example for notifications)
745 1497
		_elgg_services()->events->trigger('create', $this->type, $this);
746
		
747 1497
		_elgg_services()->events->triggerAfter('create', $this->type, $this);
748
749 1497
		return $guid;
750
	}
751
752
	/**
753
	 * Update the entity in the database.
754
	 *
755
	 * @return bool Whether the update was successful.
756
	 *
757
	 * @throws \Elgg\Exceptions\InvalidArgumentException
758
	 */
759 1383
	protected function update(): bool {
760
761 1383
		if (!$this->canEdit()) {
762 140
			return false;
763
		}
764
765
		// give old update event a chance to stop the update
766 1368
		if (!_elgg_services()->events->trigger('update', $this->type, $this)) {
767
			return false;
768
		}
769
770 1368
		$this->invalidateCache();
771
772
		// See #6225. We copy these after the update event in case a handler changed one of them.
773 1368
		$guid = (int) $this->guid;
774 1368
		$owner_guid = (int) $this->owner_guid;
775 1368
		$access_id = (int) $this->access_id;
776 1368
		$container_guid = (int) $this->container_guid;
777 1368
		$time_created = (int) $this->time_created;
778 1368
		$time = $this->getCurrentTime()->getTimestamp();
779 1368
		$deleted = $this->deleted;
780 1368
		$time_deleted = (int) $this->time_deleted;
781
782 1368
		if ($access_id == ACCESS_DEFAULT) {
783
			throw new ElggInvalidArgumentException('ACCESS_DEFAULT is not a valid access level. See its documentation in constants.php');
784
		}
785
		
786 1368
		if ($access_id == ACCESS_FRIENDS) {
787
			throw new ElggInvalidArgumentException('ACCESS_FRIENDS is not a valid access level. See its documentation in constants.php');
788
		}
789
790
		// Update primary table
791 1368
		$ret = _elgg_services()->entityTable->updateRow($guid, (object) [
792 1368
			'owner_guid' => $owner_guid,
793 1368
			'container_guid' => $container_guid,
794 1368
			'access_id' => $access_id,
795 1368
			'time_created' => $time_created,
796 1368
			'time_updated' => $time,
797 1368
			'guid' => $guid,
798 1368
			'deleted' => $deleted,
799 1368
			'time_deleted' => $time_deleted
800 1368
		]);
801 1368
		if ($ret === false) {
802
			return false;
803
		}
804
805 1368
		$this->attributes['time_updated'] = $time;
806
807 1368
		_elgg_services()->events->triggerAfter('update', $this->type, $this);
808
809 1368
		$this->orig_attributes = [];
810
811 1368
		$this->cache();
812
813
		// Handle cases where there was no error BUT no rows were updated!
814 1368
		return true;
815
	}
816
817
	/**
818
	 * Loads attributes from the entities table into the object.
819
	 *
820
	 * @param stdClass $row Object of properties from database row(s)
821
	 *
822
	 * @return bool
823
	 */
824 8633
	protected function load(stdClass $row): bool {
825 8633
		$attributes = array_merge($this->attributes, (array) $row);
826
827 8633
		if (array_diff(self::PRIMARY_ATTR_NAMES, array_keys($attributes)) !== []) {
828
			// Some primary attributes are missing
829
			return false;
830
		}
831
832 8633
		foreach ($attributes as $name => $value) {
833 8633
			if (!in_array($name, self::PRIMARY_ATTR_NAMES)) {
834 3007
				$this->setVolatileData("select:{$name}", $value);
835 3007
				unset($attributes[$name]);
836 3007
				continue;
837
			}
838
839 8633
			if (in_array($name, static::INTEGER_ATTR_NAMES)) {
840 8633
				$attributes[$name] = (int) $value;
841
			}
842
		}
843
844 8633
		$this->attributes = $attributes;
845
846 8633
		$this->cache();
847
848 8633
		return true;
849
	}
850
851
	/**
852
	 * Disable this entity.
853
	 *
854
	 * Disabled entities are not returned by getter functions.
855
	 * To enable an entity, use {@link \ElggEntity::enable()}.
856
	 *
857
	 * Recursively disabling an entity will disable all entities
858
	 * owned or contained by the parent entity.
859
	 *
860
	 * @note Internal: Disabling an entity sets the 'enabled' column to 'no'.
861
	 *
862
	 * @param string $reason    Optional reason
863
	 * @param bool   $recursive Recursively disable all contained entities?
864
	 *
865
	 * @return bool
866
	 * @see \ElggEntity::enable()
867
	 */
868 10
	public function disable(string $reason = '', bool $recursive = true): bool {
869 10
		if (!$this->guid) {
870 1
			return false;
871
		}
872
873 9
		if (!_elgg_services()->events->trigger('disable', $this->type, $this)) {
874
			return false;
875
		}
876
877 9
		if (!$this->canEdit()) {
878
			return false;
879
		}
880
881 9
		if ($this instanceof ElggUser && !$this->isBanned()) {
882
			// temporarily ban to prevent using the site during disable
883
			// not using ban function to bypass events
884 4
			$this->setMetadata('banned', 'yes');
885 4
			$unban_after = true;
886
		} else {
887 5
			$unban_after = false;
888
		}
889
890 9
		if (!empty($reason)) {
891 3
			$this->disable_reason = $reason;
892
		}
893
894 9
		$guid = (int) $this->guid;
895
896 9
		if ($recursive) {
897 5
			elgg_call(ELGG_IGNORE_ACCESS | ELGG_HIDE_DISABLED_ENTITIES | ELGG_SHOW_DELETED_ENTITIES, function () use ($guid, $reason) {
898 5
				$base_options = [
899 5
					'wheres' => [
900 5
						function(QueryBuilder $qb, $main_alias) use ($guid) {
901 5
							return $qb->compare("{$main_alias}.guid", '!=', $guid, ELGG_VALUE_GUID);
902 5
						},
903 5
					],
904 5
					'limit' => false,
905 5
					'batch' => true,
906 5
					'batch_inc_offset' => false,
907 5
				];
908
909 5
				foreach (['owner_guid', 'container_guid'] as $db_column) {
910 5
					$options = $base_options;
911 5
					$options[$db_column] = $guid;
912
913 5
					$subentities = elgg_get_entities($options);
914
					/* @var $subentity \ElggEntity */
915 5
					foreach ($subentities as $subentity) {
916 2
						if (!$subentity->isEnabled()) {
917
							continue;
918
						}
919
						
920 2
						$subentity->addRelationship($guid, 'disabled_with');
921 2
						$subentity->disable($reason, true);
922
					}
923
				}
924 5
			});
925
		}
926
927 9
		$disabled = _elgg_services()->entityTable->disable($this);
928
929 9
		if ($unban_after) {
930 4
			$this->setMetadata('banned', 'no');
931
		}
932
933 9
		if ($disabled) {
934 9
			$this->invalidateCache();
935
936 9
			$this->attributes['enabled'] = 'no';
937 9
			_elgg_services()->events->triggerAfter('disable', $this->type, $this);
938
		}
939
940 9
		return $disabled;
941
	}
942
943
	/**
944
	 * Enable the entity
945
	 *
946
	 * @param bool $recursive Recursively enable all entities disabled with the entity?
947
	 *
948
	 * @return bool
949
	 */
950 5
	public function enable(bool $recursive = true): bool {
951 5
		if (empty($this->guid)) {
952
			return false;
953
		}
954
955 5
		if (!_elgg_services()->events->trigger('enable', $this->type, $this)) {
956
			return false;
957
		}
958
959 5
		if (!$this->canEdit()) {
960
			return false;
961
		}
962
963 5
		$result = elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES | ELGG_SHOW_DELETED_ENTITIES, function() use ($recursive) {
964 5
			$result = _elgg_services()->entityTable->enable($this);
965
			
966 5
			$this->deleteMetadata('disable_reason');
967
968 5
			if ($recursive) {
969 5
				$disabled_with_it = elgg_get_entities([
970 5
					'relationship' => 'disabled_with',
971 5
					'relationship_guid' => $this->guid,
972 5
					'inverse_relationship' => true,
973 5
					'limit' => false,
974 5
					'batch' => true,
975 5
					'batch_inc_offset' => false,
976 5
				]);
977
978 5
				foreach ($disabled_with_it as $e) {
979 1
					$e->enable($recursive);
980 1
					$e->removeRelationship($this->guid, 'disabled_with');
981
				}
982
			}
983
984 5
			return $result;
985 5
		});
986
987 5
		if ($result) {
988 5
			$this->attributes['enabled'] = 'yes';
989 5
			_elgg_services()->events->triggerAfter('enable', $this->type, $this);
990
		}
991
992 5
		return $result;
993
	}
994
995
	/**
996
	 * Is this entity enabled?
997
	 *
998
	 * @return boolean Whether this entity is enabled.
999
	 */
1000 1390
	public function isEnabled(): bool {
1001 1390
		return $this->enabled == 'yes';
1002
	}
1003
1004
	/**
1005
	 * Deletes the entity.
1006
	 *
1007
	 * Removes the entity and its metadata, annotations, relationships,
1008
	 * river entries, and private data.
1009
	 *
1010
	 * Optionally can remove entities contained and owned by this entity.
1011
	 *
1012
	 * @warning If deleting recursively, this bypasses ownership of items contained by
1013
	 * the entity.  That means that if the container_guid = $this->guid, the item will
1014
	 * be deleted regardless of who owns it.
1015
	 *
1016
	 * @param bool      $recursive  If true (default) then all entities which are owned or contained by $this will also be deleted.
1017
	 * @param bool|null $persistent persistently delete the entity (default: check the 'restorable' capability)
1018
	 *
1019
	 * @return bool
1020
	 */
1021 1715
	public function delete(bool $recursive = true, ?bool $persistent = null): bool {
1022 1715
		if (!$this->canDelete()) {
1023 375
			return false;
1024
		}
1025
1026 1471
		if (!elgg_get_config('trash_enabled')) {
1027 1471
			$persistent = true;
1028
		}
1029
		
1030 1471
		if (!isset($persistent)) {
1031 3
			$persistent = !$this->hasCapability('restorable');
1032
		}
1033
		
1034
		try {
1035 1471
			if (empty($this->guid) || $persistent) {
1036 1471
				return $this->persistentDelete($recursive);
1037
			} else {
1038 8
				return $this->trash($recursive);
1039
			}
1040
		} catch (DatabaseException $ex) {
1041
			elgg_log($ex, \Psr\Log\LogLevel::ERROR);
1042
			return false;
1043
		}
1044
	}
1045
	
1046
	/**
1047
	 * Permanently delete the entity from the database
1048
	 *
1049
	 * @param bool $recursive If true (default) then all entities which are owned or contained by $this will also be deleted.
1050
	 *
1051
	 * @return bool
1052
	 * @since 6.0
1053
	 */
1054 1471
	protected function persistentDelete(bool $recursive = true): bool {
1055 1471
		return _elgg_services()->entityTable->delete($this, $recursive);
1056
	}
1057
	
1058
	/**
1059
	 * Move the entity to the trash
1060
	 *
1061
	 * @param bool $recursive If true (default) then all entities which are owned or contained by $this will also be trashed.
1062
	 *
1063
	 * @return bool
1064
	 * @since 6.0
1065
	 */
1066 8
	protected function trash(bool $recursive = true): bool {
1067 8
		$result = _elgg_services()->entityTable->trash($this, $recursive);
1068 8
		if ($result) {
1069 8
			$this->attributes['deleted'] = 'yes';
1070
		}
1071
		
1072 8
		return $result;
1073
	}
1074
	
1075
	/**
1076
	 * Restore the entity
1077
	 *
1078
	 * @param bool $recursive Recursively restores all entities trashed with the entity?
1079
	 *
1080
	 * @return bool
1081
	 * @since 6.0
1082
	 */
1083 4
	public function restore(bool $recursive = true): bool {
1084 4
		if (!$this->isDeleted()) {
1085
			return true;
1086
		}
1087
		
1088 4
		if (empty($this->guid) || !$this->canEdit()) {
1089
			return false;
1090
		}
1091
		
1092 4
		return _elgg_services()->events->triggerSequence('restore', $this->type, $this, function () use ($recursive) {
1093 4
			return elgg_call(ELGG_IGNORE_ACCESS | ELGG_SHOW_DISABLED_ENTITIES | ELGG_SHOW_DELETED_ENTITIES, function() use ($recursive) {
1094 4
				if (!_elgg_services()->entityTable->restore($this)) {
1095
					return false;
1096
				}
1097
				
1098 4
				$this->attributes['deleted'] = 'no';
1099 4
				$this->attributes['time_deleted'] = 0;
1100
				
1101 4
				$this->removeAllRelationships('deleted_by');
1102 4
				$this->removeAllRelationships('deleted_with');
1103
				
1104 4
				if (!$recursive) {
1105 1
					return true;
1106
				}
1107
				
1108 3
				set_time_limit(0);
1109
				
1110
				/* @var $deleted_with_it \ElggBatch */
1111 3
				$deleted_with_it = elgg_get_entities([
1112 3
					'relationship' => 'deleted_with',
1113 3
					'relationship_guid' => $this->guid,
1114 3
					'inverse_relationship' => true,
1115 3
					'limit' => false,
1116 3
					'batch' => true,
1117 3
					'batch_inc_offset' => false,
1118 3
				]);
1119
				
1120
				/* @var $e \ElggEntity */
1121 3
				foreach ($deleted_with_it as $e) {
1122 1
					if (!$e->restore($recursive)) {
1123
						$deleted_with_it->reportFailure();
1124
						continue;
1125
					}
1126
				}
1127
				
1128 3
				return true;
1129 4
			});
1130 4
		});
1131
	}
1132
	
1133
	/**
1134
	 * Is the entity marked as deleted
1135
	 *
1136
	 * @return bool
1137
	 */
1138 16
	public function isDeleted(): bool {
1139 16
		return $this->deleted === 'yes';
1140
	}
1141
	
1142
	/**
1143
	 * Export an entity
1144
	 *
1145
	 * @param array $params Params to pass to the event
1146
	 *
1147
	 * @return \Elgg\Export\Entity
1148
	 */
1149 71
	public function toObject(array $params = []) {
1150 71
		$object = $this->prepareObject(new \Elgg\Export\Entity());
1151
1152 71
		$params['entity'] = $this;
1153
1154 71
		return _elgg_services()->events->triggerResults('to:object', 'entity', $params, $object);
1155
	}
1156
1157
	/**
1158
	 * Prepare an object copy for toObject()
1159
	 *
1160
	 * @param \Elgg\Export\Entity $object Object representation of the entity
1161
	 *
1162
	 * @return \Elgg\Export\Entity
1163
	 */
1164 71
	protected function prepareObject(\Elgg\Export\Entity $object) {
1165 71
		$object->guid = $this->guid;
1166 71
		$object->type = $this->getType();
1167 71
		$object->subtype = $this->getSubtype();
1168 71
		$object->owner_guid = $this->getOwnerGUID();
1169 71
		$object->container_guid = $this->getContainerGUID();
1170 71
		$object->time_created = date('c', $this->getTimeCreated());
1171 71
		$object->time_updated = date('c', $this->getTimeUpdated());
1172 71
		$object->url = $this->getURL();
1173 71
		$object->read_access = (int) $this->access_id;
1174 71
		return $object;
1175
	}
1176
1177
	/**
1178
	 * Set latitude and longitude metadata tags for a given entity.
1179
	 *
1180
	 * @param float $lat  Latitude
1181
	 * @param float $long Longitude
1182
	 *
1183
	 * @return void
1184
	 */
1185 4
	public function setLatLong(float $lat, float $long): void {
1186 4
		$this->{'geo:lat'} = $lat;
1187 4
		$this->{'geo:long'} = $long;
1188
	}
1189
1190
	/**
1191
	 * Return the entity's latitude.
1192
	 *
1193
	 * @return float
1194
	 */
1195 11
	public function getLatitude(): float {
1196 11
		return (float) $this->{'geo:lat'};
1197
	}
1198
1199
	/**
1200
	 * Return the entity's longitude
1201
	 *
1202
	 * @return float
1203
	 */
1204 11
	public function getLongitude(): float {
1205 11
		return (float) $this->{'geo:long'};
1206
	}
1207
1208
	/*
1209
	 * SYSTEM LOG INTERFACE
1210
	 */
1211
1212
	/**
1213
	 * {@inheritdoc}
1214
	 */
1215 12
	public function getSystemLogID(): int {
1216 12
		return (int) $this->getGUID();
1217
	}
1218
1219
	/**
1220
	 * For a given ID, return the object associated with it.
1221
	 * This is used by the system log. It can be called on any Loggable object.
1222
	 *
1223
	 * @param int $id GUID
1224
	 *
1225
	 * @return \ElggEntity|null
1226
	 */
1227 4
	public function getObjectFromID(int $id): ?\ElggEntity {
1228 4
		return get_entity($id);
1229
	}
1230
1231
	/**
1232
	 * Update the last_action column in the entities table.
1233
	 *
1234
	 * @warning This is different to time_updated.  Time_updated is automatically set,
1235
	 * while last_action is only set when explicitly called.
1236
	 *
1237
	 * @param null|int $posted Timestamp of last action
1238
	 *
1239
	 * @return int
1240
	 * @internal
1241
	 */
1242 329
	public function updateLastAction(?int $posted = null): int {
1243 329
		$posted = _elgg_services()->entityTable->updateLastAction($this, $posted);
1244
		
1245 329
		$this->attributes['last_action'] = $posted;
1246 329
		$this->cache();
1247
		
1248 329
		return $posted;
1249
	}
1250
1251
	/**
1252
	 * Update the time_deleted column in the entities table.
1253
	 *
1254
	 * @param null|int $deleted Timestamp of deletion
1255
	 *
1256
	 * @return int
1257
	 * @internal
1258
	 */
1259 9
	public function updateTimeDeleted(?int $deleted = null): int {
1260 9
		$deleted = _elgg_services()->entityTable->updateTimeDeleted($this, $deleted);
1261
		
1262 9
		$this->attributes['time_deleted'] = $deleted;
1263 9
		$this->cache();
1264
		
1265 9
		return $deleted;
1266
	}
1267
1268
	/**
1269
	 * Disable runtime caching for entity
1270
	 *
1271
	 * @return void
1272
	 * @internal
1273
	 */
1274 1
	public function disableCaching(): void {
1275 1
		$this->_is_cacheable = false;
1276 1
		if ($this->guid) {
1277 1
			_elgg_services()->entityCache->delete($this->guid);
1278
		}
1279
	}
1280
1281
	/**
1282
	 * Enable runtime caching for entity
1283
	 *
1284
	 * @return void
1285
	 * @internal
1286
	 */
1287
	public function enableCaching(): void {
1288
		$this->_is_cacheable = true;
1289
	}
1290
1291
	/**
1292
	 * Is entity cacheable in the runtime cache
1293
	 *
1294
	 * @return bool
1295
	 * @internal
1296
	 */
1297 1931
	public function isCacheable(): bool {
1298 1931
		if (!$this->guid) {
1299
			return false;
1300
		}
1301
		
1302 1931
		if (_elgg_services()->session_manager->getIgnoreAccess()) {
1303 1540
			return false;
1304
		}
1305
		
1306 1373
		return $this->_is_cacheable;
1307
	}
1308
1309
	/**
1310
	 * Cache the entity in a session cache
1311
	 *
1312
	 * @return void
1313
	 * @internal
1314
	 */
1315 8633
	public function cache(): void {
1316 8633
		if (!$this->isCacheable()) {
1317 8618
			return;
1318
		}
1319
1320 3891
		_elgg_services()->entityCache->save($this->guid, $this);
1321
	}
1322
1323
	/**
1324
	 * Invalidate cache for entity
1325
	 *
1326
	 * @return void
1327
	 * @internal
1328
	 */
1329 1808
	public function invalidateCache(): void {
1330 1808
		if (!$this->guid) {
1331
			return;
1332
		}
1333
1334 1808
		_elgg_services()->entityCache->delete($this->guid);
1335 1808
		_elgg_services()->metadataCache->delete($this->guid);
1336
	}
1337
	
1338
	/**
1339
	 * Checks a specific capability is enabled for the entity type/subtype
1340
	 *
1341
	 * @param string $capability capability to check
1342
	 *
1343
	 * @return bool
1344
	 * @since 4.1
1345
	 */
1346 612
	public function hasCapability(string $capability): bool {
1347 612
		return _elgg_services()->entity_capabilities->hasCapability($this->getType(), $this->getSubtype(), $capability);
1348
	}
1349
	
1350
	/**
1351
	 * Returns a default set of fields to be used for forms related to this entity
1352
	 *
1353
	 * @return array
1354
	 */
1355 113
	public static function getDefaultFields(): array {
1356 113
		return [];
1357
	}
1358
	
1359
	/**
1360
	 * Helper function to easily retrieve form fields for this entity
1361
	 *
1362
	 * @return array
1363
	 */
1364 8
	final public function getFields(): array {
1365 8
		return _elgg_services()->fields->get($this->getType(), $this->getSubtype());
1366
	}
1367
}
1368