ideas   A
last analyzed

Complexity

Total Complexity 24

Size/Duplication

Total Lines 279
Duplicated Lines 0 %

Importance

Changes 34
Bugs 2 Features 1
Metric Value
wmc 24
eloc 95
c 34
b 2
f 1
dl 0
loc 279
rs 10

9 Methods

Rating   Name   Duplication   Size   Complexity  
A get_ideas() 0 32 6
A query_ideas() 0 16 2
A query_count() 0 17 2
A query_get() 0 18 2
A query_status() 0 7 2
A get_users_votes() 0 15 2
A delete_orphans() 0 30 3
A query_sort() 0 41 4
A get_idea_count() 0 3 1
1
<?php
2
/**
3
 *
4
 * Ideas extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) phpBB Limited <https://www.phpbb.com>
7
 * @license GNU General Public License, version 2 (GPL-2.0)
8
 *
9
 */
10
11
namespace phpbb\ideas\factory;
12
13
use phpbb\exception\runtime_exception;
14
use phpbb\ideas\ext;
15
16
/**
17
 * Class for handling multiple ideas
18
 */
19
class ideas extends base
20
{
21
	/** @var int */
22
	protected $idea_count;
23
24
	/** @var array */
25
	protected $sql;
26
27
	/**
28
	 * Returns an array of ideas. Defaults to ten ideas ordered by date
29
	 * excluding implemented, duplicate or invalid ideas.
30
	 *
31
	 * @param int       $number    The number of ideas to return
32
	 * @param string    $sort      A sorting option/collection
33
	 * @param string    $direction Should either be ASC or DESC
34
	 * @param array|int $status    The id of the status(es) to load
35
	 * @param int       $start     Start value for pagination
36
	 *
37
	 * @return array Array of row data
38
	 */
39
	public function get_ideas($number = 10, $sort = 'date', $direction = 'DESC', $status = [], $start = 0)
40
	{
41
		// Initialize a query to request ideas
42
		$sql = $this->query_ideas()
43
			->query_sort($sort, $direction)
44
			->query_status($status);
45
46
		// For pagination, get a count of the total ideas being requested
47
		if ($number >= $this->config['posts_per_page'])
48
		{
49
			$this->idea_count = $sql->query_count();
50
		}
51
52
		$ideas = $sql->query_get($number, $start);
53
54
		if (count($ideas))
55
		{
56
			$topic_ids = array_column($ideas, 'topic_id');
57
			$idea_ids = array_column($ideas, 'idea_id');
58
59
			$topic_tracking_info = get_complete_topic_tracking((int) $this->config['ideas_forum_id'], $topic_ids);
60
			$user_voting_info = $this->get_users_votes($this->user->data['user_id'], $idea_ids);
61
62
			foreach ($ideas as &$idea)
63
			{
64
				$idea['read'] = !(isset($topic_tracking_info[$idea['topic_id']]) && $idea['topic_last_post_time'] > $topic_tracking_info[$idea['topic_id']]);
65
				$idea['u_voted'] = isset($user_voting_info[$idea['idea_id']]) ? (int) $user_voting_info[$idea['idea_id']] : '';
66
			}
67
			unset ($idea);
68
		}
69
70
		return $ideas;
71
	}
72
73
	/**
74
	 * Initialize the $sql property with necessary SQL statements.
75
	 *
76
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
77
	 */
78
	protected function query_ideas()
79
	{
80
		$this->sql = [];
81
82
		$this->sql['SELECT'][] = 't.topic_last_post_time, t.topic_status, t.topic_visibility, i.*';
83
		$this->sql['FROM'] = "$this->table_ideas i";
84
		$this->sql['JOIN'] = "$this->table_topics t ON i.topic_id = t.topic_id";
85
		$this->sql['WHERE'][] = 't.forum_id = ' . (int) $this->config['ideas_forum_id'];
86
87
		// Only get approved topics for regular users, Moderators will see unapproved topics
88
		if (!$this->auth->acl_get('m_', $this->config['ideas_forum_id']))
89
		{
90
			$this->sql['WHERE'][] = 't.topic_visibility = ' . ITEM_APPROVED;
91
		}
92
93
		return $this;
94
	}
95
96
	/**
97
	 * Update the $sql property with ORDER BY statements to obtain
98
	 * the requested collection of Ideas. Some instances may add
99
	 * additional WHERE or SELECT statements to refine the collection.
100
	 *
101
	 * @param string $sort      A sorting option/collection
102
	 * @param string $direction Will either be ASC or DESC
103
	 *
104
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
105
	 */
106
	protected function query_sort($sort, $direction)
107
	{
108
		$sort = strtolower($sort);
109
		$direction = $direction === 'DESC' ? 'DESC' : 'ASC';
110
111
		// Most sorting relies on simple ORDER BY statements, but some may use a WHERE statement
112
		$statements = [
113
			ext::SORT_DATE    => ['ORDER_BY' => 'i.idea_date'],
114
			ext::SORT_TITLE   => ['ORDER_BY' => 'i.idea_title'],
115
			ext::SORT_AUTHOR  => ['ORDER_BY' => 'i.idea_author'],
116
			ext::SORT_SCORE   => ['ORDER_BY' => 'CAST(i.idea_votes_up AS decimal) - CAST(i.idea_votes_down AS decimal)'],
117
			ext::SORT_VOTES   => ['ORDER_BY' => 'i.idea_votes_up + i.idea_votes_down'],
118
			ext::SORT_TOP     => ['WHERE' => 'i.idea_votes_up > i.idea_votes_down'],
119
			ext::SORT_MYIDEAS => ['ORDER_BY' => 'i.idea_date', 'WHERE' => 'i.idea_author = ' . (int) $this->user->data['user_id']],
120
		];
121
122
		// Append a new WHERE statement if the sort has one
123
		if (isset($statements[$sort]['WHERE']))
124
		{
125
			$this->sql['WHERE'][] = $statements[$sort]['WHERE'];
126
		}
127
128
		// If we have an ORDER BY we use that. The absence of an ORDER BY
129
		// means we will default to sorting ideas by their calculated score.
130
		if (isset($statements[$sort]['ORDER_BY']))
131
		{
132
			$this->sql['ORDER_BY'] = "{$statements[$sort]['ORDER_BY']} $direction";
133
		}
134
		else
135
		{
136
			// https://www.evanmiller.org/how-not-to-sort-by-average-rating.html
137
			// switched SQRT(x) to POWER(x, 0.5) for SQLITE3 support
138
			$this->sql['SELECT'][] = '((i.idea_votes_up + 1.9208) / (i.idea_votes_up + i.idea_votes_down) -
139
				1.96 * POWER((i.idea_votes_up * i.idea_votes_down) / (i.idea_votes_up + i.idea_votes_down) + 0.9604, 0.5) /
140
				(i.idea_votes_up + i.idea_votes_down)) / (1 + 3.8416 / (i.idea_votes_up + i.idea_votes_down))
141
				AS ci_lower_bound';
142
143
			$this->sql['ORDER_BY'] = "ci_lower_bound $direction";
144
		}
145
146
		return $this;
147
	}
148
149
	/**
150
	 * Update $sql property with additional SQL statements to filter ideas
151
	 * by status. If $status is given we'll get those ideas. If no $status
152
	 * is given, the default is to get all ideas excluding Duplicates, Invalid
153
	 * and Implemented statuses (because they are considered done & dusted,
154
	 * if they were gases they'd be inert).
155
	 *
156
	 * @param array|int $status The id(s) of the status(es) to load
157
	 *
158
	 * @return \phpbb\ideas\factory\ideas $this For chaining calls
159
	 */
160
	protected function query_status($status = [])
161
	{
162
		$this->sql['WHERE'][] = !empty($status) ? $this->db->sql_in_set('i.idea_status', $status) : $this->db->sql_in_set(
163
			'i.idea_status', [ext::$statuses['IMPLEMENTED'], ext::$statuses['DUPLICATE'], ext::$statuses['INVALID'],
164
		], true);
165
166
		return $this;
167
	}
168
169
	/**
170
	 * Run a query using the $sql property to get a collection of ideas.
171
	 *
172
	 * @param int $number The number of ideas to return
173
	 * @param int $start  Start value for pagination
174
	 *
175
	 * @return mixed      Nested array if the query had rows, false otherwise
176
	 * @throws runtime_exception
177
	 */
178
	protected function query_get($number, $start)
179
	{
180
		if (empty($this->sql))
181
		{
182
			throw new runtime_exception('INVALID_IDEA_QUERY');
183
		}
184
185
		$sql = 'SELECT ' . implode(', ', $this->sql['SELECT']) . '
186
			FROM ' . $this->sql['FROM'] . '
187
			INNER JOIN ' . $this->sql['JOIN'] . '
188
			WHERE ' . implode(' AND ', $this->sql['WHERE']) . '
189
			ORDER BY ' . $this->sql['ORDER_BY'];
190
191
		$result = $this->db->sql_query_limit($sql, $number, $start);
192
		$rows = $this->db->sql_fetchrowset($result);
193
		$this->db->sql_freeresult($result);
194
195
		return $rows;
196
	}
197
198
	/**
199
	 * Run a query using the $sql property to get a count of ideas.
200
	 *
201
	 * @return int The number of ideas
202
	 * @throws runtime_exception
203
	 */
204
	protected function query_count()
205
	{
206
		if (empty($this->sql))
207
		{
208
			throw new runtime_exception('INVALID_IDEA_QUERY');
209
		}
210
211
		$sql = 'SELECT COUNT(i.idea_id) as count
212
			FROM ' . $this->sql['FROM'] . '
213
       		INNER JOIN ' . $this->sql['JOIN'] . '
214
			WHERE ' . implode(' AND ', $this->sql['WHERE']);
215
216
		$result = $this->db->sql_query($sql);
217
		$count = (int) $this->db->sql_fetchfield('count');
218
		$this->db->sql_freeresult($result);
219
220
		return $count;
221
	}
222
223
	/**
224
	 * Get a user's votes from a group of ideas
225
	 *
226
	 * @param int $user_id The user's id
227
	 * @param array $ids An array of idea ids
228
	 *
229
	 * @return array An array of ideas the user voted on and their vote result, or empty otherwise.
230
	 *               example: [idea_id => vote_result]
231
	 *                         1 => 1, idea 1, voted up by the user
232
	 *                         2 => 0, idea 2, voted down by the user
233
	 */
234
	public function get_users_votes($user_id, array $ids)
235
	{
236
		$results = [];
237
		$sql = 'SELECT idea_id, vote_value
238
			FROM ' . $this->table_votes . '
239
			WHERE user_id = ' . (int) $user_id . '
240
			AND ' . $this->db->sql_in_set('idea_id', $ids, false, true);
241
		$result = $this->db->sql_query($sql);
242
		while ($row = $this->db->sql_fetchrow($result))
243
		{
244
			$results[$row['idea_id']] = $row['vote_value'];
245
		}
246
		$this->db->sql_freeresult($result);
247
248
		return $results;
249
	}
250
251
	/**
252
	 * Delete orphaned ideas. Orphaned ideas may exist after a
253
	 * topic has been deleted or moved to another forum.
254
	 *
255
	 * @return int Number of rows affected
256
	 */
257
	public function delete_orphans()
258
	{
259
		// Find any orphans
260
		$sql = 'SELECT idea_id FROM ' . $this->table_ideas . '
261
 			WHERE topic_id NOT IN (SELECT t.topic_id
262
 			FROM ' . $this->table_topics . ' t
263
 				WHERE t.forum_id = ' . (int) $this->config['ideas_forum_id'] . ')';
264
		$result = $this->db->sql_query($sql);
265
		$rows = $this->db->sql_fetchrowset($result);
266
		$this->db->sql_freeresult($result);
267
268
		if (empty($rows))
269
		{
270
			return 0;
271
		}
272
273
		$this->db->sql_transaction('begin');
274
275
		foreach ($rows as $row)
276
		{
277
			// Delete idea
278
			$this->delete_idea_data($row['idea_id'], $this->table_ideas);
279
280
			// Delete votes
281
			$this->delete_idea_data($row['idea_id'], $this->table_votes);
282
		}
283
284
		$this->db->sql_transaction('commit');
285
286
		return count($rows);
287
	}
288
289
	/**
290
	 * Get the stored idea count
291
	 * Note: this should only be called after get_ideas()
292
	 *
293
	 * @return int Count of ideas
294
	 */
295
	public function get_idea_count()
296
	{
297
		return $this->idea_count ?? 0;
298
	}
299
}
300