for testing and deploying your application
for finding and fixing issues
for empowering human code reviews
<?php
use Elgg\EntityIcon;
/**
* The parent class for all Elgg Entities.
*
* An \ElggEntity is one of the basic data models in Elgg.
* It is the primary means of storing and retrieving data from the database.
* An \ElggEntity represents one row of the entities table.
* The \ElggEntity class handles CRUD operations for the entities table.
* \ElggEntity should always be extended by another class to handle CRUD
* operations on the type-specific table.
* \ElggEntity uses magic methods for get and set, so any property that isn't
* declared will be assumed to be metadata and written to the database
* as metadata on the object. All children classes must declare which
* properties are columns of the type table or they will be assumed
* to be metadata. See \ElggObject::initializeAttributes() for examples.
* Core supports 4 types of entities: \ElggObject, \ElggUser, \ElggGroup, and \ElggSite.
* @tip Plugin authors will want to extend the \ElggObject class, not this class.
* @package Elgg.Core
* @subpackage DataModel.Entities
* @property string $type object, user, group, or site (read-only after save)
* @property string $subtype Further clarifies the nature of the entity (this should not be read)
* @property-read int $guid The unique identifier for this entity (read only)
* @property int $owner_guid The GUID of the owner of this entity (usually the creator)
* @property int $container_guid The GUID of the entity containing this entity
* @property int $access_id Specifies the visibility level of this entity
* @property int $time_created A UNIX timestamp of when the entity was created
* @property-read int $time_updated A UNIX timestamp of when the entity was last updated (automatically updated on save)
* @property-read int $last_action A UNIX timestamp of when the entity was last acted upon
* @property string $enabled Is this entity enabled ('yes' or 'no')
* Metadata (the above are attributes)
* @property string $location A location of the entity
*/
abstract class ElggEntity extends \ElggData implements
Locatable, // Geocoding interface
EntityIcon // Icon interface
{
public static $primary_attr_names = [
'guid',
'type',
'subtype',
'owner_guid',
'container_guid',
'access_id',
'time_created',
'time_updated',
'last_action',
'enabled',
];
protected static $integer_attr_names = [
* Holds metadata until entity is saved. Once the entity is saved,
* metadata are written immediately to the database.
* @var array
protected $temp_metadata = [];
* Holds annotations until entity is saved. Once the entity is saved,
* annotations are written immediately to the database.
protected $temp_annotations = [];
* Holds private settings until entity is saved. Once the entity is saved,
* private settings are written immediately to the database.
protected $temp_private_settings = [];
* Volatile data structure for this object, allows for storage of data
* in-memory that isn't sync'd back to the metadata table.
protected $volatile = [];
* Holds the original (persisted) attribute values that have been changed but not yet saved.
protected $orig_attributes = [];
* @var bool
protected $_is_cacheable = true;
* Create a new entity.
* Plugin developers should only use the constructor to create a new entity.
* To retrieve entities, use get_entity() and the elgg_get_entities* functions.
* If no arguments are passed, it creates a new entity.
* If a database result is passed as a \stdClass instance, it instantiates
* that entity.
* @param stdClass $row Database row result. Default is null to create a new object.
* @throws IOException If cannot load remaining data from db
public function __construct(stdClass $row = null) {
$this->initializeAttributes();
if ($row && !$this->load($row)) {
$msg = "Failed to load new " . get_class() . " for GUID:" . $row->guid;
throw new \IOException($msg);
}
* Initialize the attributes array.
* This is vital to distinguish between metadata and base parameters.
* @return void
protected function initializeAttributes() {
parent::initializeAttributes();
$this->attributes['guid'] = null;
$this->attributes['type'] = null;
$this->attributes['subtype'] = null;
$this->attributes['owner_guid'] = _elgg_services()->session->getLoggedInUserGuid();
$this->attributes['container_guid'] = _elgg_services()->session->getLoggedInUserGuid();
$this->attributes['access_id'] = ACCESS_PRIVATE;
$this->attributes['time_updated'] = null;
$this->attributes['last_action'] = null;
$this->attributes['enabled'] = "yes";
$this->attributes['type'] = $this->getType();
* Clone an entity
* Resets the guid so that the entity can be saved as a distinct entity from
* the original. Creation time will be set when this new entity is saved.
* The owner and container guids come from the original entity. The clone
* method copies metadata but does not copy annotations or private settings.
* @note metadata will have its owner and access id set when the entity is saved
* and it will be the same as that of the entity.
public function __clone() {
$orig_entity = get_entity($this->guid);
if (!$orig_entity) {
_elgg_services()->logger->error("Failed to clone entity with GUID $this->guid");
return;
$metadata_array = elgg_get_metadata([
'guid' => $this->guid,
'limit' => 0
]);
$this->attributes['time_created'] = null;
$this->attributes['subtype'] = $orig_entity->getSubtype();
// copy metadata over to new entity - slightly convoluted due to
// handling of metadata arrays
if (is_array($metadata_array)) {
// create list of metadata names
$metadata_names = [];
foreach ($metadata_array as $metadata) {
$metadata_names[] = $metadata['name'];
// arrays are stored with multiple enties per name
$metadata_names = array_unique($metadata_names);
// move the metadata over
foreach ($metadata_names as $name) {
$this->__set($name, $orig_entity->$name);
* Set an attribute or metadata value for this entity
* Anything that is not an attribute is saved as metadata.
* @warning Metadata set this way will inherit the entity's owner and
* access ID. If you want more control over metadata, use \ElggEntity::setMetadata()
* @param string $name Name of the attribute or metadata
* @param mixed $value The value to be set
* @see \ElggEntity::setMetadata()
public function __set($name, $value) {
if ($this->$name === $value) {
// quick return if value is not changing
// Due to https://github.com/Elgg/Elgg/pull/5456#issuecomment-17785173, certain attributes
// will store empty strings as null in the DB. In the somewhat common case that we're re-setting
// the value to empty string, don't consider this a change.
if (in_array($name, ['title', 'name', 'description'])
&& $this->$name === null
&& $value === "") {
if (array_key_exists($name, $this->attributes)) {
// if an attribute is 1 (integer) and it's set to "1" (string), don't consider that a change.
if (is_int($this->attributes[$name])
&& is_string($value)
&& ((string) $this->attributes[$name] === $value)) {
// keep original values
if ($this->guid && !array_key_exists($name, $this->orig_attributes)) {
$this->orig_attributes[$name] = $this->attributes[$name];
// Certain properties should not be manually changed!
switch ($name) {
case 'guid':
case 'time_updated':
case 'last_action':
break;
case 'access_id':
case 'owner_guid':
case 'container_guid':
if ($value !== null) {
$this->attributes[$name] = (int) $value;
} else {
$this->attributes[$name] = null;
default:
$this->attributes[$name] = $value;
$this->setMetadata($name, $value);
* Get the original values of attribute(s) that have been modified since the entity was persisted.
* @return array
public function getOriginalAttributes() {
return $this->orig_attributes;
* Get an attribute or metadata value
* If the name matches an attribute, the attribute is returned. If metadata
* does not exist with that name, a null is returned.
* This only returns an array if there are multiple values for a particular
* $name key.
* @return mixed
public function __get($name) {
return $this->attributes[$name];
return $this->getMetadata($name);
* Get the entity's display name
* @return string The title or name of this entity.
public function getDisplayName() {
return $this->name;
* Sets the title or name of this entity.
* @param string $display_name The title or name of this entity.
public function setDisplayName($display_name) {
$this->name = $display_name;
* Return the value of a piece of metadata.
* @param string $name Name
* @return mixed The value, or null if not found.
public function getMetadata($name) {
$guid = $this->guid;
if (!$guid) {
if (isset($this->temp_metadata[$name])) {
// md is returned as an array only if more than 1 entry
if (count($this->temp_metadata[$name]) == 1) {
return $this->temp_metadata[$name][0];
return $this->temp_metadata[$name];
return null;
// upon first cache miss, just load/cache all the metadata and retry.
// if this works, the rest of this function may not be needed!
$cache = _elgg_services()->metadataCache;
if ($cache->isLoaded($guid)) {
return $cache->getSingle($guid, $name);
$cache->populateFromEntities([$guid]);
// in case ignore_access was on, we have to check again...
$md = elgg_get_metadata([
'guid' => $guid,
'metadata_name' => $name,
'limit' => 0,
'distinct' => false,
$value = null;
if ($md && !is_array($md)) {
$value = $md->value;
} elseif (count($md) == 1) {
$value = $md[0]->value;
} else if ($md && is_array($md)) {
$value = metadata_array_to_values($md);
return $value;
* Unset a property from metadata or attribute.
* @warning If you use this to unset an attribute, you must save the object!
* @param string $name The name of the attribute or metadata.
* @todo some attributes should be set to null or other default values
public function __unset($name) {
$this->attributes[$name] = "";
$this->deleteMetadata($name);
* Set metadata on this entity.
* Plugin developers usually want to use the magic set method ($entity->name = 'value').
* Use this method if you want to explicitly set the owner or access of the metadata.
* You cannot set the owner/access before the entity has been saved.
* @param string $name Name of the metadata
* @param mixed $value Value of the metadata (doesn't support assoc arrays)
* @param string $value_type 'text', 'integer', or '' for automatic detection
* @param bool $multiple Allow multiple values for a single name.
* Does not support associative arrays.
* @return bool
* @throws InvalidArgumentException
public function setMetadata($name, $value, $value_type = '', $multiple = false) {
// normalize value to an array that we will loop over
// remove indexes if value already an array.
if (is_array($value)) {
$value = array_values($value);
$value = [$value];
// strip null values from array
$value = array_filter($value, function($var) {
return !is_null($var);
});
if (empty($this->guid)) {
// unsaved entity. store in temp array
return $this->setTempMetadata($name, $value, $multiple);
// saved entity. persist md to db.
if (!$multiple) {
$current_metadata = $this->getMetadata($name);
if ((is_array($current_metadata) || count($value) > 1 || $value === []) && isset($current_metadata)) {
// remove current metadata if needed
// need to remove access restrictions right now to delete
// because this is the expected behavior
$ia = elgg_set_ignore_access(true);
$delete_result = elgg_delete_metadata([
'limit' => false,
elgg_set_ignore_access($ia);
if (false === $delete_result) {
return false;
if (count($value) > 1) {
// new value is a multiple valued metadata
$multiple = true;
// create new metadata
foreach ($value as $value_tmp) {
$metadata = new ElggMetadata();
$metadata->entity_guid = $this->guid;
$metadata->name = $name;
$metadata->value_type = $value_type;
$metadata->value = $value_tmp;
$md_id = _elgg_services()->metadataTable->create($metadata, $multiple);
if (!$md_id) {
return true;
* Set temp metadata on this entity.
protected function setTempMetadata($name, $value, $multiple = false) {
// if overwrite, delete first
unset($this->temp_metadata[$name]);
if (count($value)) {
// only save if value array contains data
$this->temp_metadata[$name] = $value;
if (!isset($this->temp_metadata[$name])) {
$this->temp_metadata[$name] = [];
$this->temp_metadata[$name] = array_merge($this->temp_metadata[$name], $value);
* Deletes all metadata on this object (metadata.entity_guid = $this->guid).
* If you pass a name, only metadata matching that name will be deleted.
* @warning Calling this with no $name will clear all metadata on the entity.
* @param null|string $name The name of the metadata to remove.
* @return bool|null
* @since 1.8
public function deleteMetadata($name = null) {
if (!$this->guid) {
// remove from temp_metadata
if ($name) {
$this->temp_metadata = [];
$options = [
$options['metadata_name'] = $name;
return elgg_delete_metadata($options);
* Get a piece of volatile (non-persisted) data on this entity.
* @param string $name The name of the volatile data
* @return mixed The value or null if not found.
public function getVolatileData($name) {
return array_key_exists($name, $this->volatile) ? $this->volatile[$name] : null;
* Set a piece of volatile (non-persisted) data on this entity
* @param mixed $value Value
public function setVolatileData($name, $value) {
$this->volatile[$name] = $value;
* Remove all relationships to and from this entity.
* If you pass a relationship name, only relationships matching that name
* will be deleted.
* @warning Calling this with no $relationship will clear all relationships
* for this entity.
* @param null|string $relationship The name of the relationship to remove.
* @see \ElggEntity::addRelationship()
* @see \ElggEntity::removeRelationship()
public function deleteRelationships($relationship = null) {
$relationship = (string) $relationship;
$result = remove_entity_relationships($this->getGUID(), $relationship);
return $result && remove_entity_relationships($this->getGUID(), $relationship, true);
* Add a relationship between this an another entity.
* @tip Read the relationship like "This entity is a $relationship of $guid_two."
* @param int $guid_two GUID of the target entity of the relationship.
* @param string $relationship The type of relationship.
* @see \ElggEntity::deleteRelationships()
public function addRelationship($guid_two, $relationship) {
return add_entity_relationship($this->getGUID(), $relationship, $guid_two);
* Remove a relationship
public function removeRelationship($guid_two, $relationship) {
return remove_entity_relationship($this->getGUID(), $relationship, $guid_two);
* Adds a private setting to this entity.
* Private settings are similar to metadata but will not
* be searched and there are fewer helper functions for them.
* @param string $name Name of private setting
* @param mixed $value Value of private setting
public function setPrivateSetting($name, $value) {
if ((int) $this->guid > 0) {
return set_private_setting($this->getGUID(), $name, $value);
$this->temp_private_settings[$name] = $value;
* Returns a private setting value
* @param string $name Name of the private setting
* @return mixed Null if the setting does not exist
public function getPrivateSetting($name) {
if ((int) ($this->guid) > 0) {
return get_private_setting($this->getGUID(), $name);
if (isset($this->temp_private_settings[$name])) {
return $this->temp_private_settings[$name];
* Removes private setting
public function removePrivateSetting($name) {
return remove_private_setting($this->getGUID(), $name);
* Deletes all annotations on this object (annotations.entity_guid = $this->guid).
* If you pass a name, only annotations matching that name will be deleted.
* @warning Calling this with no or empty arguments will clear all annotations on the entity.
* @param null|string $name The annotations name to remove.
public function deleteAnnotations($name = null) {
$options['annotation_name'] = $name;
return elgg_delete_annotations($options);
* Deletes all annotations owned by this object (annotations.owner_guid = $this->guid).
* @param null|string $name The name of annotations to delete.
public function deleteOwnedAnnotations($name = null) {
// access is turned off for this because they might
// no longer have access to an entity they created annotations on.
'annotation_owner_guid' => $this->guid,
$r = elgg_delete_annotations($options);
return $r;
* Disables annotations for this entity, optionally based on name.
* @param string $name An options name of annotations to disable.
public function disableAnnotations($name = '') {
return elgg_disable_annotations($options);
* Enables annotations for this entity, optionally based on name.
* @warning Before calling this, you must use {@link access_show_hidden_entities()}
* @param string $name An options name of annotations to enable.
public function enableAnnotations($name = '') {
return elgg_enable_annotations($options);
* Helper function to return annotation calculation results
* @param string $name The annotation name.
* @param string $calculation A valid MySQL function to run its values through
private function getAnnotationCalculation($name, $calculation) {
'guid' => $this->getGUID(),
'annotation_name' => $name,
'annotation_calculation' => $calculation
return elgg_get_annotations($options);
* Adds an annotation to an entity.
* @warning By default, annotations are private.
* @warning Annotating an unsaved entity more than once with the same name
* will only save the last annotation.
* @todo Update temp_annotations to store an instance of ElggAnnotation and simply call ElggAnnotation::save(),
* after entity is saved
* @param string $name Annotation name
* @param mixed $value Annotation value
* @param int $access_id Access ID
* @param int $owner_guid GUID of the annotation owner
* @param string $value_type The type of annotation value
* @return bool|int Returns int if an annotation is saved
public function annotate($name, $value, $access_id = ACCESS_PRIVATE, $owner_guid = 0, $value_type = "") {
if ($this->guid) {
if (!$owner_guid) {
$owner_guid = elgg_get_logged_in_user_guid();
$annotation = new ElggAnnotation();
$annotation->entity_guid = $this->guid;
$annotation->name = $name;
$annotation->value_type = $value_type;
$annotation->value = $value;
$annotation->owner_guid = $owner_guid;
$annotation->access_id = $access_id;
return $annotation->save();
$this->temp_annotations[$name] = $value;
* Gets an array of annotations.
* To retrieve annotations on an unsaved entity, pass array('name' => [annotation name])
* as the options array.
* @param array $options Array of options for elgg_get_annotations() except guid.
* @see elgg_get_annotations()
public function getAnnotations(array $options = []) {
$options['guid'] = $this->guid;
$name = elgg_extract('annotation_name', $options, '');
if (isset($this->temp_annotations[$name])) {
return [$this->temp_annotations[$name]];
return [];
* Count annotations.
* @param string $name The type of annotation.
* @return int
public function countAnnotations($name = "") {
return $this->getAnnotationCalculation($name, 'count');
* Get the average of an integer type annotation.
public function getAnnotationsAvg($name) {
return $this->getAnnotationCalculation($name, 'avg');
* Get the sum of integer type annotations of a given name.
public function getAnnotationsSum($name) {
return $this->getAnnotationCalculation($name, 'sum');
* Get the minimum of integer type annotations of given name.
public function getAnnotationsMin($name) {
return $this->getAnnotationCalculation($name, 'min');
* Get the maximum of integer type annotations of a given name.
public function getAnnotationsMax($name) {
return $this->getAnnotationCalculation($name, 'max');
* Count the number of comments attached to this entity.
* @return int Number of comments
* @since 1.8.0
public function countComments() {
$params = ['entity' => $this];
$num = _elgg_services()->hooks->trigger('comments:count', $this->getType(), $params);
if (is_int($num)) {
return $num;
return elgg_get_entities([
'type' => 'object',
'subtype' => 'comment',
'container_guid' => $this->getGUID(),
'count' => true,
* Returns the ACLs owned by the entity
* @param array $options additional options to get the access collections with
* @return \ElggAccessCollection[]
* @see elgg_get_access_collections()
* @since 3.0
public function getOwnedAccessCollections($options = []) {
$options['owner_guid'] = $this->guid;
return _elgg_services()->accessCollections->getEntityCollections($options);
* Returns the first ACL owned by the entity with a given subtype
* @param string $subtype subtype of the ACL
* @return \ElggAccessCollection|false
public function getOwnedAccessCollection($subtype) {
if (!is_string($subtype) || $subtype === '') {
$acls = $this->getOwnedAccessCollections([
'subtype' => $subtype,
return elgg_extract(0, $acls, false);
* Gets an array of entities with a relationship to this entity.
* @param array $options Options array. See elgg_get_entities_from_relationship()
* for a list of options. 'relationship_guid' is set to
* this entity.
* @return array|false An array of entities or false on failure
* @see elgg_get_entities_from_relationship()
public function getEntitiesFromRelationship(array $options = []) {
$options['relationship_guid'] = $this->guid;
return elgg_get_entities($options);
* Gets the number of entities from a specific relationship type
* @param string $relationship Relationship type (eg "friends")
* @param bool $inverse_relationship Invert relationship
* @return int|false The number of entities or false on failure
public function countEntitiesFromRelationship($relationship, $inverse_relationship = false) {
'relationship' => $relationship,
'relationship_guid' => $this->getGUID(),
'inverse_relationship' => $inverse_relationship,
'count' => true
* Can a user edit this entity?
* @tip Can be overridden by registering for the permissions_check plugin hook.
* @param int $user_guid The user GUID, optionally (default: logged in user)
* @return bool Whether this entity is editable by the given user.
* @see elgg_set_ignore_access()
public function canEdit($user_guid = 0) {
return _elgg_services()->userCapabilities->canEdit($this, $user_guid);
* Can a user delete this entity?
* @tip Can be overridden by registering for the permissions_check:delete plugin hook.
* @return bool Whether this entity is deletable by the given user.
* @since 1.11
public function canDelete($user_guid = 0) {
return _elgg_services()->userCapabilities->canDelete($this, $user_guid);
* Can a user edit metadata on this entity?
* If no specific metadata is passed, it returns whether the user can
* edit any metadata on the entity.
* @tip Can be overridden by by registering for the permissions_check:metadata
* plugin hook.
* @param \ElggMetadata $metadata The piece of metadata to specifically check or null for any metadata
public function canEditMetadata($metadata = null, $user_guid = 0) {
return _elgg_services()->userCapabilities->canEditMetadata($this, $user_guid, $metadata);
* Can a user add an entity to this container
* @param int $user_guid The GUID of the user creating the entity (0 for logged in user).
* @param string $type The type of entity we're looking to write
* @param string $subtype The subtype of the entity we're looking to write
public function canWriteToContainer($user_guid = 0, $type = 'all', $subtype = 'all') {
return _elgg_services()->userCapabilities->canWriteToContainer($this, $user_guid, $type, $subtype);
* Can a user comment on an entity?
* @tip Can be overridden by registering for the permissions_check:comment,
* <entity type> plugin hook.
* @param int $user_guid User guid (default is logged in user)
* @param bool $default Default permission
public function canComment($user_guid = 0, $default = null) {
return _elgg_services()->userCapabilities->canComment($this, $user_guid, $default);
* Can a user annotate an entity?
* @tip Can be overridden by registering for the plugin hook [permissions_check:annotate:<name>,
* <entity type>] or [permissions_check:annotate, <entity type>]. The hooks are called in that order.
* @tip If you want logged out users to annotate an object, do not call
* canAnnotate(). It's easier than using the plugin hook.
* @param string $annotation_name The name of the annotation (default is unspecified)
public function canAnnotate($user_guid = 0, $annotation_name = '') {
return _elgg_services()->userCapabilities->canAnnotate($this, $user_guid, $annotation_name);
* Returns the access_id.
* @return int The access ID
public function getAccessID() {
return $this->access_id;
* Returns the guid.
* @return int|null GUID
public function getGUID() {
return $this->guid;
* Returns the entity type
* @return string The entity type
public function getType() {
// this is just for the PHPUnit mocking framework
return $this->type;
* Get the entity subtype
* @return string The entity subtype
public function getSubtype() {
return $this->attributes['subtype'];
* Get the guid of the entity's owner.
* @return int The owner GUID
public function getOwnerGUID() {
return (int) $this->owner_guid;
* Gets the \ElggEntity that owns this entity.
* @return \ElggEntity The owning entity
public function getOwnerEntity() {
return get_entity($this->owner_guid);
* Set the container for this object.
* @param int $container_guid The ID of the container.
public function setContainerGUID($container_guid) {
return $this->container_guid = (int) $container_guid;
* Gets the container GUID for this entity.
public function getContainerGUID() {
return (int) $this->container_guid;
* Get the container entity for this object.
* @return \ElggEntity
public function getContainerEntity() {
return get_entity($this->getContainerGUID());
* Returns the UNIX epoch time that this entity was last updated
* @return int UNIX epoch time
public function getTimeUpdated() {
return $this->time_updated;
* Gets the URL for this entity.
* Plugins can register for the 'entity:url', <type> plugin hook to
* customize the url for an entity.
* @return string The URL of the entity
public function getURL() {
$url = _elgg_services()->hooks->trigger('entity:url', $this->getType(), ['entity' => $this]);
if ($url === null || $url === '' || $url === false) {