aboutsummaryrefslogtreecommitdiff
path: root/engine/classes/ElggAttributeLoader.php
blob: ffc80b02d006a340cb6178b8b39086e1f9f02474 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
<?php

/**
 * Loads ElggEntity attributes from DB or validates those passed in via constructor
 *
 * @access private
 *
 * @package    Elgg.Core
 * @subpackage DataModel
 */
class ElggAttributeLoader {

	/**
	 * @var array names of attributes in all entities
	 */
	protected static $primary_attr_names = array(
		'guid',
		'type',
		'subtype',
		'owner_guid',
		'container_guid',
		'site_guid',
		'access_id',
		'time_created',
		'time_updated',
		'last_action',
		'enabled',
	);

	/**
	 * @var array names of secondary attributes required for the entity
	 */
	protected $secondary_attr_names = array();

	/**
	 * @var string entity type (not class) required for fetched primaries
	 */
	protected $required_type;

	/**
	 * @var array
	 */
	protected $initialized_attributes;

	/**
	 * @var string class of object being loaded
	 */
	protected $class;

	/**
	 * @var bool should access control be considered when fetching entity?
	 */
	public $requires_access_control = true;

	/**
	 * @var callable function used to load attributes from {prefix}entities table
	 */
	public $primary_loader = 'get_entity_as_row';

	/**
	 * @var callable function used to load attributes from secondary table
	 */
	public $secondary_loader = '';

	/**
	 * @var callable function used to load all necessary attributes
	 */
	public $full_loader = '';

	/**
	 * Constructor
	 *
	 * @param string $class             class of object being loaded
	 * @param string $required_type     entity type this is being used to populate
	 * @param array  $initialized_attrs attributes after initializeAttributes() has been run
	 * @throws InvalidArgumentException
	 */
	public function __construct($class, $required_type, array $initialized_attrs) {
		if (!is_string($class)) {
			throw new InvalidArgumentException('$class must be a class name.');
		}
		$this->class = $class;

		if (!is_string($required_type)) {
			throw new InvalidArgumentException('$requiredType must be a system entity type.');
		}
		$this->required_type = $required_type;

		$this->initialized_attributes = $initialized_attrs;
		unset($initialized_attrs['tables_split'], $initialized_attrs['tables_loaded']);
		$all_attr_names = array_keys($initialized_attrs);
		$this->secondary_attr_names = array_diff($all_attr_names, self::$primary_attr_names);
	}

	/**
	 * Get primary attributes missing that are missing
	 *
	 * @param stdClass $row Database row
	 * @return array
	 */
	protected function isMissingPrimaries($row) {
		return array_diff(self::$primary_attr_names, array_keys($row)) !== array();
	}

	/**
	 * Get secondary attributes that are missing
	 *
	 * @param stdClass $row Database row
	 * @return array
	 */
	protected function isMissingSecondaries($row) {
		return array_diff($this->secondary_attr_names, array_keys($row)) !== array();
	}

	/**
	 * Check that the type is correct
	 *
	 * @param stdClass $row Database row
	 * @return void
	 * @throws InvalidClassException
	 */
	protected function checkType($row) {
		if ($row['type'] !== $this->required_type) {
			$msg = elgg_echo('InvalidClassException:NotValidElggStar', array($row['guid'], $this->class));
			throw new InvalidClassException($msg);
		}
	}

	/**
	 * Get all required attributes for the entity, validating any that are passed in. Returns empty array
	 * if can't be loaded (Check $failure_reason).
	 *
	 * This function splits loading between "primary" attributes (those in {prefix}entities table) and
	 * "secondary" attributes (e.g. those in {prefix}objects_entity), but can load all at once if a
	 * combined loader is available.
	 *
	 * @param mixed $row a row loaded from DB (array or stdClass) or a GUID
	 * @return array will be empty if failed to load all attributes (access control or entity doesn't exist)
	 *
	 * @throws InvalidArgumentException|LogicException|IncompleteEntityException
	 */
	public function getRequiredAttributes($row) {
		if (!is_array($row) && !($row instanceof stdClass)) {
			// assume row is the GUID
			$row = array('guid' => $row);
		}
		$row = (array) $row;
		if (empty($row['guid'])) {
			throw new InvalidArgumentException('$row must be or contain a GUID');
		}

		// these must be present to support isFullyLoaded()
		foreach (array('tables_split', 'tables_loaded') as $key) {
			if (isset($this->initialized_attributes[$key])) {
				$row[$key] = $this->initialized_attributes[$key];
			}
		}

		$was_missing_primaries = $this->isMissingPrimaries($row);
		$was_missing_secondaries = $this->isMissingSecondaries($row);

		// some types have a function to load all attributes at once, it should be faster
		if (($was_missing_primaries || $was_missing_secondaries) && is_callable($this->full_loader)) {
			$fetched = (array) call_user_func($this->full_loader, $row['guid']);
			if (!$fetched) {
				return array();
			}
			$row = array_merge($row, $fetched);
			$this->checkType($row);
		} else {
			if ($was_missing_primaries) {
				if (!is_callable($this->primary_loader)) {
					throw new LogicException('Primary attribute loader must be callable');
				}
				if ($this->requires_access_control) {
					$fetched = (array) call_user_func($this->primary_loader, $row['guid']);
				} else {
					$ignoring_access = elgg_set_ignore_access();
					$fetched = (array) call_user_func($this->primary_loader, $row['guid']);
					elgg_set_ignore_access($ignoring_access);
				}
				if (!$fetched) {
					return array();
				}
				$row = array_merge($row, $fetched);
			}

			// We must test type before trying to load the secondaries so that InvalidClassException
			// gets thrown. Otherwise the secondary loader will fail and return false.
			$this->checkType($row);

			if ($was_missing_secondaries) {
				if (!is_callable($this->secondary_loader)) {
					throw new LogicException('Secondary attribute loader must be callable');
				}
				$fetched = (array) call_user_func($this->secondary_loader, $row['guid']);
				if (!$fetched) {
					if ($row['type'] === 'site') {
						// A special case is needed for sites: When vanilla ElggEntities are created and
						// saved, these are stored w/ type "site", but with no sites_entity row. These
						// are probably only created in the unit tests.
						// @todo Don't save vanilla ElggEntities with type "site"

						$row = $this->filterAddedColumns($row);
						$row['guid'] = (int) $row['guid'];
						return $row;
					}
					throw new IncompleteEntityException("Secondary loader failed to return row for {$row['guid']}");
				}
				$row = array_merge($row, $fetched);
			}
		}

		$row = $this->filterAddedColumns($row);

		// Note: If there are still missing attributes, we're running on a 1.7 or earlier schema. We let
		// this pass so the upgrades can run.

		// guid needs to be an int  https://github.com/elgg/elgg/issues/4111
		$row['guid'] = (int) $row['guid'];

		return $row;
	}

	/**
	 * Filter out keys returned by the query which should not appear in the entity's attributes
	 *
	 * @param array $row All columns from the query
	 * @return array Columns acceptable for the entity's attributes
	 */
	protected function filterAddedColumns($row) {
		// make an array with keys as acceptable attribute names
		$acceptable_attrs = self::$primary_attr_names;
		array_splice($acceptable_attrs, count($acceptable_attrs), 0, $this->secondary_attr_names);
		$acceptable_attrs = array_combine($acceptable_attrs, $acceptable_attrs);

		// @todo remove these when #4584 is in place
		$acceptable_attrs['tables_split'] = true;
		$acceptable_attrs['tables_loaded'] = true;

		foreach ($row as $key => $val) {
			if (!isset($acceptable_attrs[$key])) {
				unset($row[$key]);
			}
		}
		return $row;
	}
}