Test Failed
Branch master (4a3c5b)
by Greg
12:31
created

IndividualListController   F

Complexity

Total Complexity 116

Size/Duplication

Total Lines 540
Duplicated Lines 50 %

Importance

Changes 0
Metric Value
dl 270
loc 540
rs 1.5789
c 0
b 0
f 0
wmc 116

10 Methods

Rating   Name   Duplication   Size   Complexity  
D initialLetter() 44 44 19
F surnameAlpha() 7 63 12
A givenNameInitial() 8 8 2
D getInitialSql() 38 38 20
F givenAlpha() 33 69 16
A surnameInitial() 11 11 3
D getAlphabetForLocale() 80 80 22
C surnames() 17 38 8
C individuals() 14 53 11
A families() 10 10 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like IndividualListController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use IndividualListController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2017 webtrees development team
5
 * This program is free software: you can redistribute it and/or modify
6
 * it under the terms of the GNU General Public License as published by
7
 * the Free Software Foundation, either version 3 of the License, or
8
 * (at your option) any later version.
9
 * This program is distributed in the hope that it will be useful,
10
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
 * GNU General Public License for more details.
13
 * You should have received a copy of the GNU General Public License
14
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15
 */
16
namespace Fisharebest\Webtrees\Controller;
17
18
use Fisharebest\Webtrees\Database;
19
use Fisharebest\Webtrees\Family;
20
use Fisharebest\Webtrees\Filter;
21
use Fisharebest\Webtrees\Html;
22
use Fisharebest\Webtrees\I18N;
23
use Fisharebest\Webtrees\Individual;
24
25
/**
26
 * Controller for the individual list
27
 */
28
class IndividualListController extends PageController {
29
	/**
30
	 * Get a list of initial letters, for lists of names.
31
	 *
32
	 * @param string $locale Return the alphabet for this locale
33
	 *
34
	 * @return string[]
35
	 */
36 View Code Duplication
	private function getAlphabetForLocale($locale) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
37
		switch ($locale) {
38
		case 'ar':
39
			return [
40
				'ا', 'ب', 'ت', 'ث', 'ج', 'ح', 'خ', 'د', 'ذ', 'ر', 'ز', 'س', 'ش', 'ص', 'ض', 'ط', 'ظ', 'ع', 'غ', 'ف', 'ق', 'ك', 'ل', 'م', 'ن', 'ه', 'و', 'ي', 'آ', 'ة', 'ى', 'ی',
41
			];
42
		case 'cs':
43
			return [
44
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'CH', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
45
			];
46
		case 'da':
47
		case 'nb':
48
		case 'nn':
49
			return [
50
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Æ', 'Ø', 'Å',
51
			];
52
		case 'el':
53
			return [
54
				'Α', 'Β', 'Γ', 'Δ', 'Ε', 'Ζ', 'Η', 'Θ', 'Ι', 'Κ', 'Λ', 'Μ', 'Ν', 'Ξ', 'Ο', 'Π', 'Ρ', 'Σ', 'Τ', 'Υ', 'Φ', 'Χ', 'Ψ', 'Ω',
55
			];
56
		case 'es':
57
			return [
58
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'Ñ', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
59
			];
60
		case 'et':
61
			return [
62
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'Z', 'Ž', 'T', 'U', 'V', 'W', 'Õ', 'Ä', 'Ö', 'Ü', 'X', 'Y',
63
			];
64
		case 'fi':
65
		case 'sv':
66
			return [
67
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Å', 'Ä', 'Ö',
68
			];
69
		case 'he':
70
			return [
71
				'א', 'ב', 'ג', 'ד', 'ה', 'ו', 'ז', 'ח', 'ט', 'י', 'כ', 'ל', 'מ', 'נ', 'ס', 'ע', 'פ', 'צ', 'ק', 'ר', 'ש', 'ת',
72
			];
73
		case 'hu':
74
			return [
75
				'A', 'B', 'C', 'CS', 'D', 'DZ', 'DZS', 'E', 'F', 'G', 'GY', 'H', 'I', 'J', 'K', 'L', 'LY', 'M', 'N', 'NY', 'O', 'Ö', 'P', 'Q', 'R', 'S', 'SZ', 'T', 'TY', 'U', 'Ü', 'V', 'W', 'X', 'Y', 'Z', 'ZS',
76
			];
77
		case 'lt':
78
			return [
79
				'A', 'Ą', 'B', 'C', 'Č', 'D', 'E', 'Ę', 'Ė', 'F', 'G', 'H', 'I', 'Y', 'Į', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'Š', 'T', 'U', 'Ų', 'Ū', 'V', 'Z', 'Ž',
80
			];
81
		case 'nl':
82
			return [
83
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'IJ',
84
			];
85
		case 'pl':
86
			return [
87
				'A', 'B', 'C', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'Ł', 'M', 'N', 'O', 'Ó', 'P', 'Q', 'R', 'S', 'Ś', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ź', 'Ż',
88
			];
89
		case 'ro':
90
			return [
91
				'A', 'Ă', 'Â', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'Î', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Ş', 'T', 'Ţ', 'U', 'V', 'W', 'X', 'Y', 'Z',
92
			];
93
		case 'ru':
94
			return [
95
				'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й', 'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф', 'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ъ', 'Ы', 'Ь', 'Э', 'Ю', 'Я',
96
			];
97
		case 'sk':
98
			return [
99
				'A', 'Á', 'Ä', 'B', 'C', 'Č', 'D', 'Ď', 'E', 'É', 'F', 'G', 'H', 'I', 'Í', 'J', 'K', 'L', 'Ľ', 'Ĺ', 'M', 'N', 'Ň', 'O', 'Ó', 'Ô', 'P', 'Q', 'R', 'Ŕ', 'S', 'Š', 'T', 'Ť', 'U', 'Ú', 'V', 'W', 'X', 'Y', 'Ý', 'Z', 'Ž',
100
			];
101
		case 'sl':
102
			return [
103
				'A', 'B', 'C', 'Č', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ž',
104
			];
105
		case 'sr':
106
			return [
107
				'A', 'B', 'C', 'Č', 'Ć', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'Š', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'Ž',
108
			];
109
		case 'tr':
110
			return [
111
				'A', 'B', 'C', 'Ç', 'D', 'E', 'F', 'G', 'Ğ', 'H', 'I', 'İ', 'J', 'K', 'L', 'M', 'N', 'O', 'Ö', 'P', 'R', 'S', 'Ş', 'T', 'U', 'Ü', 'V', 'Y', 'Z',
112
			];
113
		default:
114
			return [
115
				'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
116
			];
117
		}
118
	}
119
120
	/**
121
	 * Get the initial letter of a name, taking care of multi-letter sequences and equivalences.
122
	 *
123
	 * @param string $name
124
	 *
125
	 * @return string
126
	 */
127 View Code Duplication
	public function initialLetter($name) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
128
		$name = I18N::strtoupper($name);
129
		switch (WT_LOCALE) {
130
		case 'cs':
131
			if (substr($name, 0, 2) == 'CH') {
132
				return 'CH';
133
			}
134
			break;
135
		case 'da':
136
		case 'nb':
137
		case 'nn':
138
			if (substr($name, 0, 2) == 'AA') {
139
				return 'Å';
140
			}
141
			break;
142
		case 'hu':
143
			if (substr($name, 0, 2) == 'CS') {
144
				return 'CS';
145
			} elseif (substr($name, 0, 3) == 'DZS') {
146
				return 'DZS';
147
			} elseif (substr($name, 0, 2) == 'DZ') {
148
				return 'DZ';
149
			} elseif (substr($name, 0, 2) == 'GY') {
150
				return 'GY';
151
			} elseif (substr($name, 0, 2) == 'LY') {
152
				return 'LY';
153
			} elseif (substr($name, 0, 2) == 'NY') {
154
				return 'NY';
155
			} elseif (substr($name, 0, 2) == 'SZ') {
156
				return 'SZ';
157
			} elseif (substr($name, 0, 2) == 'TY') {
158
				return 'TY';
159
			} elseif (substr($name, 0, 2) == 'ZS') {
160
				return 'ZS';
161
			}
162
			break;
163
		case 'nl':
164
			if (substr($name, 0, 2) == 'IJ') {
165
				return 'IJ';
166
			}
167
			break;
168
		}
169
		// No special rules - just take the first character
170
		return mb_substr($name, 0, 1);
171
	}
172
173
	/**
174
	 * Generate SQL to match a given letter, taking care of cases that
175
	 * are not covered by the collation setting.
176
	 *
177
	 * We must consider:
178
	 * potential substrings, such as Czech "CH" and "C"
179
	 * equivalent letters, such as Danish "AA" and "Å"
180
	 *
181
	 * We COULD write something that handles all languages generically,
182
	 * but its performance would most likely be poor.
183
	 *
184
	 * For languages that don't appear in this list, we could write
185
	 * simpler versions of the surnameAlpha() and givenAlpha() functions,
186
	 * but it gives no noticable improvement in performance.
187
	 *
188
	 * @param string $field
189
	 * @param string $letter
190
	 *
191
	 * @return string
192
	 */
193 View Code Duplication
	private function getInitialSql($field, $letter) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
194
		switch (WT_LOCALE) {
195
		case 'cs':
196
			switch ($letter) {
197
			case 'C': return $field . " LIKE 'C%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'CH%' COLLATE " . I18N::collation();
198
			}
199
			break;
200
		case 'da':
201
		case 'nb':
202
		case 'nn':
203
			switch ($letter) {
204
				// AA gets listed under Å
205
			case 'A': return $field . " LIKE 'A%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'AA%' COLLATE " . I18N::collation();
206
			case 'Å': return "(" . $field . " LIKE 'Å%' COLLATE " . I18N::collation() . " OR " . $field . " LIKE 'AA%' COLLATE " . I18N::collation() . ")";
207
			}
208
			break;
209
		case 'hu':
210
			switch ($letter) {
211
			case 'C':  return $field . " LIKE 'C%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'CS%' COLLATE " . I18N::collation();
212
			case 'D':  return $field . " LIKE 'D%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'DZ%' COLLATE " . I18N::collation();
213
			case 'DZ': return $field . " LIKE 'DZ%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'DZS%' COLLATE " . I18N::collation();
214
			case 'G':  return $field . " LIKE 'G%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'GY%' COLLATE " . I18N::collation();
215
			case 'L':  return $field . " LIKE 'L%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'LY%' COLLATE " . I18N::collation();
216
			case 'N':  return $field . " LIKE 'N%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'NY%' COLLATE " . I18N::collation();
217
			case 'S':  return $field . " LIKE 'S%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'SZ%' COLLATE " . I18N::collation();
218
			case 'T':  return $field . " LIKE 'T%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'TY%' COLLATE " . I18N::collation();
219
			case 'Z':  return $field . " LIKE 'Z%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'ZS%' COLLATE " . I18N::collation();
220
			}
221
			break;
222
		case 'nl':
223
			switch ($letter) {
224
			case 'I': return $field . " LIKE 'I%' COLLATE " . I18N::collation() . " AND " . $field . " NOT LIKE 'IJ%' COLLATE " . I18N::collation();
225
			}
226
			break;
227
		}
228
229
		// Easy cases: the MySQL collation rules take care of it
230
		return "$field LIKE CONCAT('@'," . Database::quote($letter) . ",'%') COLLATE " . I18N::collation() . " ESCAPE '@'";
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Database::quote() has been deprecated: We should use bind-variables instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

230
		return "$field LIKE CONCAT('@'," . /** @scrutinizer ignore-deprecated */ Database::quote($letter) . ",'%') COLLATE " . I18N::collation() . " ESCAPE '@'";

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
231
	}
232
233
	/**
234
	 * Get a list of initial surname letters for indilist.php and famlist.php
235
	 *
236
	 * @param bool $marnm  if set, include married names
237
	 * @param bool $fams   if set, only consider individuals with FAMS records
238
	 * @param bool $totals if set, count the number of names beginning with each letter
239
	 *
240
	 * @return int[]
241
	 */
242
	public function surnameAlpha($marnm, $fams, $totals = true) {
243
		$alphas = [];
244
245
		$sql =
246
			"SELECT SQL_CACHE COUNT(n_id)" .
247
			" FROM `##name` " .
248
			($fams ? " JOIN `##link` ON (n_id=l_from AND n_file=l_file AND l_type='FAMS') " : "") .
249
			" WHERE n_file=" . $this->tree()->getTreeId() .
250
			($marnm ? "" : " AND n_type!='_MARNM'");
251
252
		// Fetch all the letters in our alphabet, whether or not there
253
		// are any names beginning with that letter. It looks better to
254
		// show the full alphabet, rather than omitting rare letters such as X
255 View Code Duplication
		foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) {
256
			$count = 1;
257
			if ($totals) {
258
				$count = Database::prepare($sql . " AND " . $this->getInitialSql('n_surn', $letter))->fetchOne();
259
			}
260
			$alphas[$letter] = $count;
261
		}
262
263
		// Now fetch initial letters that are not in our alphabet,
264
		// including "@" (for "@N.N.") and "" for no surname.
265
		$sql =
266
			"SELECT SQL_CACHE initial, count FROM (SELECT UPPER(LEFT(n_surn, 1)) AS initial, COUNT(n_id) AS count" .
267
			" FROM `##name` " .
268
			($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") .
269
			" WHERE n_file = :tree_id AND n_surn <> ''" .
270
			($marnm ? "" : " AND n_type != '_MARNM'");
271
272
		$args = [
273
			'tree_id' => $this->tree()->getTreeId(),
274
		];
275
276
		foreach ($this->getAlphabetForLocale(WT_LOCALE) as $n => $letter) {
277
			$sql .= " AND n_surn COLLATE :collate_" . $n . " NOT LIKE :letter_" . $n;
278
			$args['collate_' . $n] = I18N::collation();
279
			$args['letter_' . $n]  = $letter . '%';
280
		}
281
		$sql .= " GROUP BY UPPER(LEFT(n_surn, 1))) AS subquery ORDER BY initial = '', initial = '@', initial";
282
		foreach (Database::prepare($sql)->execute($args)->fetchAssoc() as $alpha => $count) {
283
			$alphas[$alpha] = $count;
284
		}
285
286
		// Names with no surname
287
		$sql =
288
			"SELECT SQL_CACHE COUNT(n_id)" .
289
			" FROM `##name` " .
290
			($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") .
291
			" WHERE n_file = :tree_id AND n_surn = ''" .
292
			($marnm ? "" : " AND n_type != '_MARNM'");
293
294
		$args = [
295
			'tree_id' => $this->tree()->getTreeId(),
296
		];
297
298
		$count_no_surname = (int) Database::prepare($sql)->execute($args)->fetchOne();
299
		if ($count_no_surname !== 0) {
300
			// Special code to indicate "no surname"
301
			$alphas[','] = $count_no_surname;
302
		}
303
304
		return $alphas;
305
	}
306
307
	/**
308
	 * Get a list of initial given name letters for indilist.php and famlist.php
309
	 *
310
	 * @param string $surn   if set, only consider people with this surname
311
	 * @param string $salpha if set, only consider surnames starting with this letter
312
	 * @param bool   $marnm  if set, include married names
313
	 * @param bool   $fams   if set, only consider individuals with FAMS records
314
	 *
315
	 * @return int[]
316
	 */
317
	public function givenAlpha($surn, $salpha, $marnm, $fams) {
318
		$alphas = [];
319
320
		$sql =
321
			"SELECT SQL_CACHE COUNT(DISTINCT n_id)" .
322
			" FROM `##name`" .
323
			($fams ? " JOIN `##link` ON (n_id=l_from AND n_file=l_file AND l_type='FAMS') " : "") .
324
			" WHERE n_file=" . $this->tree()->getTreeId() . " " .
325
			($marnm ? "" : " AND n_type!='_MARNM'");
326
327 View Code Duplication
		if ($surn) {
328
			$sql .= " AND n_surn=" . Database::quote($surn) . " COLLATE '" . I18N::collation() . "'";
0 ignored issues
show
Deprecated Code introduced by
The function Fisharebest\Webtrees\Database::quote() has been deprecated: We should use bind-variables instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

328
			$sql .= " AND n_surn=" . /** @scrutinizer ignore-deprecated */ Database::quote($surn) . " COLLATE '" . I18N::collation() . "'";

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
329
		} elseif ($salpha == ',') {
330
			$sql .= " AND n_surn=''";
331
		} elseif ($salpha == '@') {
332
			$sql .= " AND n_surn='@N.N.'";
333
		} elseif ($salpha) {
334
			$sql .= " AND " . $this->getInitialSql('n_surn', $salpha);
335
		} else {
336
			// All surnames
337
			$sql .= " AND n_surn NOT IN ('', '@N.N.')";
338
		}
339
340
		// Fetch all the letters in our alphabet, whether or not there
341
		// are any names beginning with that letter. It looks better to
342
		// show the full alphabet, rather than omitting rare letters such as X
343 View Code Duplication
		foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) {
344
			$count           = Database::prepare($sql . " AND " . $this->getInitialSql('n_givn', $letter))->fetchOne();
345
			$alphas[$letter] = $count;
346
		}
347
348
		// Now fetch initial letters that are not in our alphabet,
349
		// including "@" (for "@N.N.") and "" for no surname
350
		$sql =
351
			"SELECT SQL_CACHE initial, total FROM (SELECT UPPER(LEFT(n_givn, 1)) AS initial, COUNT(DISTINCT n_id) AS total" .
352
			" FROM `##name` " .
353
			($fams ? " JOIN `##link` ON (n_id = l_from AND n_file = l_file AND l_type = 'FAMS') " : "") .
354
			" WHERE n_file = :tree_id" .
355
			($marnm ? "" : " AND n_type != '_MARNM'");
356
357
		$args = [
358
			'tree_id' => $this->tree()->getTreeId(),
359
		];
360
361 View Code Duplication
		if ($surn) {
362
			$sql .= " AND n_surn COLLATE :collate_1 = :surn";
363
			$args['collate_1'] = I18N::collation();
364
			$args['surn']      = $surn;
365
		} elseif ($salpha === ',') {
366
			$sql .= " AND n_surn = ''";
367
		} elseif ($salpha === '@') {
368
			$sql .= " AND n_surn = '@N.N.'";
369
		} elseif ($salpha) {
370
			$sql .= " AND " . $this->getInitialSql('n_surn', $salpha);
371
		} else {
372
			// All surnames
373
			$sql .= " AND n_surn NOT IN ('', '@N.N.')";
374
		}
375
376 View Code Duplication
		foreach ($this->getAlphabetForLocale(WT_LOCALE) as $letter) {
377
			$sql .= " AND n_givn NOT LIKE '" . $letter . "%' COLLATE " . I18N::collation();
378
		}
379
		$sql .= " GROUP BY UPPER(LEFT(n_givn, 1))) AS subquery ORDER BY initial = '@', initial = '', initial";
380
381
		foreach (Database::prepare($sql)->execute($args)->fetchAssoc() as $alpha => $count) {
382
			$alphas[$alpha] = $count;
383
		}
384
385
		return $alphas;
386
	}
387
388
	/**
389
	 * Get a list of actual surnames and variants, based on a "root" surname.
390
	 *
391
	 * @param string $surn   if set, only fetch people with this surname
392
	 * @param string $salpha if set, only consider surnames starting with this letter
393
	 * @param bool   $marnm  if set, include married names
394
	 * @param bool   $fams   if set, only consider individuals with FAMS records
395
	 *
396
	 * @return array
397
	 */
398
	public function surnames($surn, $salpha, $marnm, $fams) {
399
		$sql =
400
			"SELECT SQL_CACHE n2.n_surn, n1.n_surname, n1.n_id" .
401
			" FROM `##name` n1 " .
402
			($fams ? " JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") .
403
			" JOIN (SELECT n_surn COLLATE :collate_0 AS n_surn, n_file FROM `##name`" .
404
			" WHERE n_file = :tree_id" .
405
			($marnm ? "" : " AND n_type != '_MARNM'");
406
407
		$args = [
408
			'tree_id'   => $this->tree()->getTreeId(),
409
			'collate_0' => I18N::collation(),
410
		];
411
412 View Code Duplication
		if ($surn) {
413
			$sql .= " AND n_surn COLLATE :collate_1 = :surn";
414
			$args['collate_1'] = I18N::collation();
415
			$args['surn']      = $surn;
416
		} elseif ($salpha === ',') {
417
			$sql .= " AND n_surn = ''";
418
		} elseif ($salpha === '@') {
419
			$sql .= " AND n_surn = '@N.N.'";
420
		} elseif ($salpha) {
421
			$sql .= " AND " . $this->getInitialSql('n_surn', $salpha);
422
		} else {
423
			// All surnames
424
			$sql .= " AND n_surn NOT IN ('', '@N.N.')";
425
		}
426
		$sql .= " GROUP BY n_surn COLLATE :collate_2, n_file) AS n2 ON (n1.n_surn = n2.n_surn COLLATE :collate_3 AND n1.n_file = n2.n_file)";
427
		$args['collate_2'] = I18N::collation();
428
		$args['collate_3'] = I18N::collation();
429
430
		$list = [];
431 View Code Duplication
		foreach (Database::prepare($sql)->execute($args)->fetchAll() as $row) {
432
			$list[I18N::strtoupper($row->n_surn)][$row->n_surname][$row->n_id] = true;
433
		}
434
435
		return $list;
436
	}
437
438
	/**
439
	 * Fetch a list of individuals with specified names
440
	 *
441
	 * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
442
	 * To search for names with no surnames, use $salpha=","
443
	 *
444
	 * @param string $surn   if set, only fetch people with this surname
445
	 * @param string $salpha if set, only fetch surnames starting with this letter
446
	 * @param string $galpha if set, only fetch given names starting with this letter
447
	 * @param bool   $marnm  if set, include married names
448
	 * @param bool   $fams   if set, only fetch individuals with FAMS records
449
	 *
450
	 * @return Individual[]
451
	 */
452
	public function individuals($surn, $salpha, $galpha, $marnm, $fams) {
453
		$sql =
454
			"SELECT i_id AS xref, i_gedcom AS gedcom, n_full " .
455
			"FROM `##individuals` " .
456
			"JOIN `##name` ON n_id = i_id AND n_file = i_file " .
457
			($fams ? "JOIN `##link` ON n_id = l_from AND n_file = l_file AND l_type = 'FAMS' " : "") .
458
			"WHERE n_file = :tree_id " .
459
			($marnm ? "" : "AND n_type != '_MARNM'");
460
461
		$args = [
462
			'tree_id' => $this->tree()->getTreeId(),
463
		];
464
465 View Code Duplication
		if ($surn) {
466
			$sql .= " AND n_surn COLLATE :collate_1 = :surn";
467
			$args['collate_1'] = I18N::collation();
468
			$args['surn']      = $surn;
469
		} elseif ($salpha === ',') {
470
			$sql .= " AND n_surn = ''";
471
		} elseif ($salpha === '@') {
472
			$sql .= " AND n_surn = '@N.N.'";
473
		} elseif ($salpha) {
474
			$sql .= " AND " . $this->getInitialSql('n_surn', $salpha);
475
		} else {
476
			// All surnames
477
			$sql .= " AND n_surn NOT IN ('', '@N.N.')";
478
		}
479
		if ($galpha) {
480
			$sql .= " AND " . $this->getInitialSql('n_givn', $galpha);
481
		}
482
483
		$sql .= " ORDER BY CASE n_surn WHEN '@N.N.' THEN 1 ELSE 0 END, n_surn COLLATE :collate_2, CASE n_givn WHEN '@P.N.' THEN 1 ELSE 0 END, n_givn COLLATE :collate_3";
484
		$args['collate_2'] = I18N::collation();
485
		$args['collate_3'] = I18N::collation();
486
487
		$list = [];
488
		$rows = Database::prepare($sql)->execute($args)->fetchAll();
489
		foreach ($rows as $row) {
490
			$person = Individual::getInstance($row->xref, $this->tree(), $row->gedcom);
491
			// The name from the database may be private - check the filtered list...
492
			foreach ($person->getAllNames() as $n => $name) {
493
				if ($name['fullNN'] == $row->n_full) {
494
					$person->setPrimaryName($n);
495
					// We need to clone $person, as we may have multiple references to the
496
					// same person in this list, and the "primary name" would otherwise
497
					// be shared amongst all of them.
498
					$list[] = clone $person;
499
					break;
500
				}
501
			}
502
		}
503
504
		return $list;
505
	}
506
507
	/**
508
	 * Fetch a list of families with specified names
509
	 *
510
	 * To search for unknown names, use $surn="@N.N.", $salpha="@" or $galpha="@"
511
	 * To search for names with no surnames, use $salpha=","
512
	 *
513
	 * @param string $surn   if set, only fetch people with this surname
514
	 * @param string $salpha if set, only fetch surnames starting with this letter
515
	 * @param string $galpha if set, only fetch given names starting with this letter
516
	 * @param bool   $marnm  if set, include married names
517
	 *
518
	 * @return Family[]
519
	 */
520 View Code Duplication
	public function families($surn, $salpha, $galpha, $marnm) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
521
		$list = [];
522
		foreach ($this->individuals($surn, $salpha, $galpha, $marnm, true) as $indi) {
523
			foreach ($indi->getSpouseFamilies() as $family) {
524
				$list[$family->getXref()] = $family;
525
			}
526
		}
527
		usort($list, '\Fisharebest\Webtrees\GedcomRecord::compare');
528
529
		return $list;
530
	}
531
532
	/**
533
	 * Some initial letters have a special meaning
534
	 *
535
	 * @param string $initial
536
	 *
537
	 * @return string
538
	 */
539 View Code Duplication
	public function givenNameInitial($initial) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
540
		switch ($initial) {
541
		case '@':
542
			return I18N::translateContext('Unknown given name', '…');
543
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
544
		default:
545
			return Html::escape($initial);
546
			break;
547
		}
548
	}
549
550
	/**
551
	 * Some initial letters have a special meaning
552
	 *
553
	 * @param string $initial
554
	 *
555
	 * @return string
556
	 */
557 View Code Duplication
	public function surnameInitial($initial) {
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
558
		switch ($initial) {
559
		case '@':
560
			return I18N::translateContext('Unknown surname', '…');
561
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
562
		case ',':
563
			return I18N::translate('None');
564
			break;
565
		default:
566
			return Html::escape($initial);
567
			break;
568
		}
569
	}
570
}
571