Passed
Push — 3.0 ( 82aee8...a1e00e )
by Jeroen
58:43 queued 10s
created

classes/Elgg/Database/RelationshipsTable.php (1 issue)

Severity
1
<?php
2
namespace Elgg\Database;
3
4
use Elgg\Database;
5
use Elgg\Database\Clauses\GroupByClause;
6
use Elgg\Database\Clauses\OrderByClause;
7
use Elgg\Database\Clauses\SelectClause;
8
use Elgg\EventsService;
9
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
10
11
/**
12
 * WARNING: API IN FLUX. DO NOT USE DIRECTLY.
13
 *
14
 * @access private
15
 *
16
 * @package    Elgg.Core
17
 * @subpackage Database
18
 * @since      1.10.0
19
 */
20
class RelationshipsTable {
21
22
	use \Elgg\TimeUsing;
23
	
24
	/**
25
	 * @var Database
26
	 */
27
	protected $db;
28
29
	/**
30
	 * @var EntityTable
31
	 */
32
	protected $entities;
33
34
	/**
35
	 * @var MetadataTable
36
	 */
37
	protected $metadata;
38
39
	/**
40
	 * @var EventsService
41
	 */
42
	protected $events;
43
44
	/**
45
	 * Constructor
46
	 *
47
	 * @param Database      $db       Elgg Database
48
	 * @param EntityTable   $entities Entity table
49
	 * @param MetadataTable $metadata Metadata table
50
	 * @param EventsService $events   Events service
51
	 */
52 4863
	public function __construct(Database $db, EntityTable $entities, MetadataTable $metadata, EventsService $events) {
53 4863
		$this->db = $db;
54 4863
		$this->entities = $entities;
55 4863
		$this->metadata = $metadata;
56 4863
		$this->events = $events;
57 4863
	}
58
59
	/**
60
	 * Get a relationship by its ID
61
	 *
62
	 * @param int $id The relationship ID
63
	 *
64
	 * @return \ElggRelationship|false False if not found
65
	 */
66 137
	public function get($id) {
67 137
		$row = $this->getRow($id);
68 137
		if (!$row) {
69
			return false;
70
		}
71
72 137
		return new \ElggRelationship($row);
73
	}
74
75
	/**
76
	 * Get a database row from the relationship table
77
	 *
78
	 * @param int $id The relationship ID
79
	 *
80
	 * @return \stdClass|false False if no row found
81
	 * @access private
82
	 */
83 137
	public function getRow($id) {
84 137
		$sql = "SELECT * FROM {$this->db->prefix}entity_relationships WHERE id = :id";
85
		$params = [
86 137
			':id' => (int) $id,
87
		];
88 137
		return $this->db->getDataRow($sql, null, $params);
89
	}
90
91
	/**
92
	 * Delete a relationship by its ID
93
	 *
94
	 * @param int  $id         Relationship ID
95
	 * @param bool $call_event Call the delete event before deleting
96
	 *
97
	 * @return bool
98
	 */
99 19
	public function delete($id, $call_event = true) {
100 19
		$id = (int) $id;
101
102 19
		$relationship = $this->get($id);
103
104 19
		if ($call_event && !$this->events->trigger('delete', 'relationship', $relationship)) {
105 1
			return false;
106
		}
107
108 18
		$sql = "DELETE FROM {$this->db->prefix}entity_relationships WHERE id = :id";
109
		$params = [
110 18
			':id' => $id,
111
		];
112 18
		return (bool) $this->db->deleteData($sql, $params);
113
	}
114
115
	/**
116
	 * Create a relationship between two entities. E.g. friendship, group membership, site membership.
117
	 *
118
	 * This function lets you make the statement "$guid_one is a $relationship of $guid_two". In the statement,
119
	 * $guid_one is the subject of the relationship, $guid_two is the target, and $relationship is the type.
120
	 *
121
	 * @param int    $guid_one     GUID of the subject entity of the relationship
122
	 * @param string $relationship Type of the relationship
123
	 * @param int    $guid_two     GUID of the target entity of the relationship
124
	 * @param bool   $return_id    Return the ID instead of bool?
125
	 *
126
	 * @return bool|int
127
	 * @throws \InvalidArgumentException
128
	 */
129 135
	public function add($guid_one, $relationship, $guid_two, $return_id = false) {
130 135
		if (strlen($relationship) > \ElggRelationship::RELATIONSHIP_LIMIT) {
131
			$msg = "relationship name cannot be longer than " . \ElggRelationship::RELATIONSHIP_LIMIT;
132
			throw new \InvalidArgumentException($msg);
133
		}
134
135
		// Check for duplicates
136
		// note: escape $relationship after this call, we don't want to double-escape
137 135
		if ($this->check($guid_one, $relationship, $guid_two)) {
138
			return false;
139
		}
140
		
141
		$sql = "
142 135
			INSERT INTO {$this->db->prefix}entity_relationships
143
			       (guid_one, relationship, guid_two, time_created)
144
			VALUES (:guid1, :relationship, :guid2, :time)
145
		";
146
		$params = [
147 135
			':guid1' => (int) $guid_one,
148 135
			':guid2' => (int) $guid_two,
149 135
			':relationship' => $relationship,
150 135
			':time' => $this->getCurrentTime()->getTimestamp(),
151
		];
152
153
		try {
154 135
			$id = $this->db->insertData($sql, $params);
155 135
			if (!$id) {
156 135
				return false;
157
			}
158
		} catch (\DatabaseException $e) {
159
			$prev = $e->getPrevious();
160
			if ($prev instanceof UniqueConstraintViolationException) {
161
				// duplicate key error see https://github.com/Elgg/Elgg/issues/9179
162
				return false;
163
			}
164
			throw $e;
165
		}
166
167 135
		$obj = $this->get($id);
168
169 135
		$result = $this->events->trigger('create', 'relationship', $obj);
170 135
		if (!$result) {
171 1
			$this->delete($id, false);
172 1
			return false;
173
		}
174
175 134
		return $return_id ? $obj->id : true;
176
	}
177
178
	/**
179
	 * Check if a relationship exists between two entities. If so, the relationship object is returned.
180
	 *
181
	 * This function lets you ask "Is $guid_one a $relationship of $guid_two?"
182
	 *
183
	 * @param int    $guid_one     GUID of the subject entity of the relationship
184
	 * @param string $relationship Type of the relationship
185
	 * @param int    $guid_two     GUID of the target entity of the relationship
186
	 *
187
	 * @return \ElggRelationship|false Depending on success
188
	 */
189 152
	public function check($guid_one, $relationship, $guid_two) {
190
		$query = "
191 152
			SELECT * FROM {$this->db->prefix}entity_relationships
192
			WHERE guid_one = :guid1
193
			  AND relationship = :relationship
194
			  AND guid_two = :guid2
195
			LIMIT 1
196
		";
197
		$params = [
198 152
			':guid1' => (int) $guid_one,
199 152
			':guid2' => (int) $guid_two,
200 152
			':relationship' => $relationship,
201
		];
202 152
		$row = $this->rowToElggRelationship($this->db->getDataRow($query, null, $params));
203 152
		if ($row) {
204 47
			return $row;
205
		}
206
207 149
		return false;
208
	}
209
210
	/**
211
	 * Delete a relationship between two entities.
212
	 *
213
	 * This function lets you say "$guid_one is no longer a $relationship of $guid_two."
214
	 *
215
	 * @param int    $guid_one     GUID of the subject entity of the relationship
216
	 * @param string $relationship Type of the relationship
217
	 * @param int    $guid_two     GUID of the target entity of the relationship
218
	 *
219
	 * @return bool
220
	 */
221 22
	public function remove($guid_one, $relationship, $guid_two) {
222 22
		$obj = $this->check($guid_one, $relationship, $guid_two);
223 22
		if (!$obj instanceof \ElggRelationship) {
1 ignored issue
show
$obj is never a sub-type of ElggRelationship.
Loading history...
224 9
			return false;
225
		}
226
227 15
		return $this->delete($obj->id);
228
	}
229
230
	/**
231
	 * Removes all relationships originating from a particular entity
232
	 *
233
	 * @param int    $guid                 GUID of the subject or target entity (see $inverse)
234
	 * @param string $relationship         Type of the relationship (optional, default is all relationships)
235
	 * @param bool   $inverse_relationship Is $guid the target of the deleted relationships? By default, $guid is the
236
	 *                                     subject of the relationships.
237
	 * @param string $type                 The type of entity related to $guid (defaults to all)
238
	 *
239
	 * @return true
240
	 */
241 251
	public function removeAll($guid, $relationship = "", $inverse_relationship = false, $type = '') {
242 251
		$guid = (int) $guid;
243 251
		$params = [];
244
245 251
		if (!empty($relationship)) {
246 1
			$where = "AND er.relationship = :relationship";
247 1
			$params[':relationship'] = $relationship;
248
		} else {
249 251
			$where = "";
250
		}
251
252 251
		if (!empty($type)) {
253
			if (!$inverse_relationship) {
254
				$join = "JOIN {$this->db->prefix}entities e ON e.guid = er.guid_two";
255
			} else {
256
				$join = "JOIN {$this->db->prefix}entities e ON e.guid = er.guid_one";
257
				$where .= " AND ";
258
			}
259
			$where .= " AND e.type = :type";
260
			$params[':type'] = $type;
261
		} else {
262 251
			$join = "";
263
		}
264
265 251
		$guid_col = $inverse_relationship ? "guid_two" : "guid_one";
266
267 251
		$this->db->deleteData("
268 251
			DELETE er FROM {$this->db->prefix}entity_relationships AS er
269 251
			$join
270 251
			WHERE $guid_col = $guid
271 251
			$where
272 251
		", $params);
273
274 251
		return true;
275
	}
276
277
	/**
278
	 * Get all the relationships for a given GUID.
279
	 *
280
	 * @param int  $guid                 GUID of the subject or target entity (see $inverse)
281
	 * @param bool $inverse_relationship Is $guid the target of the deleted relationships? By default $guid is
282
	 *                                   the subject of the relationships.
283
	 *
284
	 * @return \ElggRelationship[]
285
	 */
286
	public function getAll($guid, $inverse_relationship = false) {
287
		$where = ($inverse_relationship ? "guid_two = :guid" : "guid_one = :guid");
288
289
		$query = "SELECT *
290
			FROM {$this->db->prefix}entity_relationships
291
			WHERE {$where}";
292
		$params = [
293
			':guid' => (int) $guid,
294
		];
295
296
		return $this->db->getData($query, [$this, 'rowToElggRelationship'], $params);
297
	}
298
299
	/**
300
	 * Gets the number of entities by a the number of entities related to them in a particular way.
301
	 * This is a good way to get out the users with the most friends, or the groups with the
302
	 * most members.
303
	 *
304
	 * @param array $options An options array compatible with elgg_get_entities()
305
	 *
306
	 * @return \ElggEntity[]|int|boolean If count, int. If not count, array. false on errors.
307
	 */
308 1
	public function getEntitiesFromCount(array $options = []) {
309 1
		$options['selects'][] = new SelectClause("COUNT(e.guid) AS total");
310 1
		$options['group_by'][] = new GroupByClause('r.guid_two');
311 1
		$options['order_by'][] = new OrderByClause('total', 'desc');
312
313 1
		return Entities::find($options);
314
	}
315
316
	/**
317
	 * Convert a database row to a new \ElggRelationship
318
	 *
319
	 * @param \stdClass $row Database row from the relationship table
320
	 *
321
	 * @return \ElggRelationship|false
322
	 * @access private
323
	 */
324 152
	public function rowToElggRelationship($row) {
325 152
		if ($row instanceof \stdClass) {
326 47
			return new \ElggRelationship($row);
327
		}
328
329 149
		return false;
330
	}
331
}
332