Completed
Push — develop ( 9087a8...c9b4ef )
by Greg
16:31 queued 05:44
created

Family   F

Complexity

Total Complexity 65

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 388
rs 3.3333
c 0
b 0
f 0
wmc 65

20 Methods

Rating   Name   Duplication   Size   Complexity  
A getAllMarriagePlaces() 0 9 3
A canShowName() 0 4 1
A getMarriage() 0 2 1
A getSpouses() 0 4 1
A formatListDetails() 0 4 1
A getMarriageYear() 0 2 1
A getSpouse() 0 5 2
A fetchGedcomRecord() 0 7 1
A getMarriageDate() 0 6 2
A getMarriagePlace() 0 4 1
A compareMarrDate() 0 2 1
A getNumberOfChildren() 0 7 2
A getAllMarriageDates() 0 8 3
A getHusband() 0 7 4
A getWife() 0 7 4
C getAllNames() 0 69 14
B __construct() 0 18 8
B createPrivateGedcomRecord() 0 14 5
A canShowByType() 0 11 4
B getChildren() 0 16 6

How to fix   Complexity   

Complex Class

Complex classes like Family 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 Family, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * webtrees: online genealogy
4
 * Copyright (C) 2018 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;
17
18
/**
19
 * A GEDCOM family (FAM) object.
20
 */
21
class Family extends GedcomRecord {
22
	const RECORD_TYPE = 'FAM';
23
	const ROUTE_NAME  = 'family';
24
25
	/** @var Individual|null The husband (or first spouse for same-sex couples) */
26
	private $husb;
27
28
	/** @var Individual|null The wife (or second spouse for same-sex couples) */
29
	private $wife;
30
31
	/**
32
	 * Create a GedcomRecord object from raw GEDCOM data.
33
	 *
34
	 * @param string      $xref
35
	 * @param string      $gedcom  an empty string for new/pending records
36
	 * @param string|null $pending null for a record with no pending edits,
37
	 *                             empty string for records with pending deletions
38
	 * @param Tree        $tree
39
	 */
40
	public function __construct($xref, $gedcom, $pending, $tree) {
41
		parent::__construct($xref, $gedcom, $pending, $tree);
42
43
		// Fetch family members
44
		if (preg_match_all('/^1 (?:HUSB|WIFE|CHIL) @(.+)@/m', $gedcom . $pending, $match)) {
45
			Individual::load($tree, $match[1]);
46
		}
47
48
		if (preg_match('/^1 HUSB @(.+)@/m', $gedcom . $pending, $match)) {
49
			$this->husb = Individual::getInstance($match[1], $tree);
0 ignored issues
show
Documentation Bug introduced by
It seems like Fisharebest\Webtrees\Ind...tance($match[1], $tree) can also be of type Fisharebest\Webtrees\Note or Fisharebest\Webtrees\Source or Fisharebest\Webtrees\Repository or Fisharebest\Webtrees\Media or Fisharebest\Webtrees\Family. However, the property $husb is declared as type Fisharebest\Webtrees\Individual|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
50
		}
51
		if (preg_match('/^1 WIFE @(.+)@/m', $gedcom . $pending, $match)) {
52
			$this->wife = Individual::getInstance($match[1], $tree);
0 ignored issues
show
Documentation Bug introduced by
It seems like Fisharebest\Webtrees\Ind...tance($match[1], $tree) can also be of type Fisharebest\Webtrees\Note or Fisharebest\Webtrees\Source or Fisharebest\Webtrees\Repository or Fisharebest\Webtrees\Media or Fisharebest\Webtrees\Family. However, the property $wife is declared as type Fisharebest\Webtrees\Individual|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
53
		}
54
55
		// Make sure husb/wife are the right way round.
56
		if ($this->husb && $this->husb->getSex() === 'F' || $this->wife && $this->wife->getSex() === 'M') {
57
			list($this->husb, $this->wife) = [$this->wife, $this->husb];
58
		}
59
	}
60
61
	/**
62
	 * Generate a private version of this record
63
	 *
64
	 * @param int $access_level
65
	 *
66
	 * @return string
67
	 */
68
	protected function createPrivateGedcomRecord($access_level) {
69
		$SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS');
70
71
		$rec = '0 @' . $this->xref . '@ FAM';
72
		// Just show the 1 CHIL/HUSB/WIFE tag, not any subtags, which may contain private data
73
		preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . WT_REGEX_XREF . ')@/', $this->gedcom, $matches, PREG_SET_ORDER);
74
		foreach ($matches as $match) {
75
			$rela = Individual::getInstance($match[1], $this->tree);
76
			if ($rela && ($SHOW_PRIVATE_RELATIONSHIPS || $rela->canShow($access_level))) {
77
				$rec .= $match[0];
78
			}
79
		}
80
81
		return $rec;
82
	}
83
84
	/**
85
	 * Fetch data from the database
86
	 *
87
	 * @param string $xref
88
	 * @param int    $tree_id
89
	 *
90
	 * @return null|string
91
	 */
92
	protected static function fetchGedcomRecord($xref, $tree_id) {
93
		return Database::prepare(
94
			"SELECT f_gedcom FROM `##families` WHERE f_id = :xref AND f_file = :tree_id"
95
		)->execute([
96
			'xref'    => $xref,
97
			'tree_id' => $tree_id,
98
		])->fetchOne();
99
	}
100
101
	/**
102
	 * Get the male (or first female) partner of the family
103
	 *
104
	 * @param $access_level int|null
105
	 *
106
	 * @return Individual|null
107
	 */
108
	public function getHusband($access_level = null) {
109
		$SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS');
110
111
		if ($this->husb && ($SHOW_PRIVATE_RELATIONSHIPS || $this->husb->canShowName($access_level))) {
112
			return $this->husb;
113
		} else {
114
			return null;
115
		}
116
	}
117
118
	/**
119
	 * Get the female (or second male) partner of the family
120
	 *
121
	 * @param $access_level int|null
122
	 *
123
	 * @return Individual|null
124
	 */
125
	public function getWife($access_level = null) {
126
		$SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS');
127
128
		if ($this->wife && ($SHOW_PRIVATE_RELATIONSHIPS || $this->wife->canShowName($access_level))) {
129
			return $this->wife;
130
		} else {
131
			return null;
132
		}
133
	}
134
135
	/**
136
	 * Each object type may have its own special rules, and re-implement this function.
137
	 *
138
	 * @param int $access_level
139
	 *
140
	 * @return bool
141
	 */
142
	protected function canShowByType($access_level) {
143
		// Hide a family if any member is private
144
		preg_match_all('/\n1 (?:CHIL|HUSB|WIFE) @(' . WT_REGEX_XREF . ')@/', $this->gedcom, $matches);
145
		foreach ($matches[1] as $match) {
146
			$person = Individual::getInstance($match, $this->tree);
147
			if ($person && !$person->canShow($access_level)) {
148
				return false;
149
			}
150
		}
151
152
		return true;
153
	}
154
155
	/**
156
	 * Can the name of this record be shown?
157
	 *
158
	 * @param int|null $access_level
159
	 *
160
	 * @return bool
161
	 */
162
	public function canShowName($access_level = null) {
163
		// We can always see the name (Husband-name + Wife-name), however,
164
		// the name will often be "private + private"
165
		return true;
166
	}
167
168
	/**
169
	 * Find the spouse of a person.
170
	 *
171
	 * @param Individual $person
172
	 * @param int|null   $access_level
173
	 *
174
	 * @return Individual|null
175
	 */
176
	public function getSpouse(Individual $person, $access_level = null) {
177
		if ($person === $this->wife) {
178
			return $this->getHusband($access_level);
179
		} else {
180
			return $this->getWife($access_level);
181
		}
182
	}
183
184
	/**
185
	 * Get the (zero, one or two) spouses from this family.
186
	 *
187
	 * @param int|null $access_level
188
	 *
189
	 * @return Individual[]
190
	 */
191
	public function getSpouses($access_level = null) {
192
		return array_filter([
193
			$this->getHusband($access_level),
194
			$this->getWife($access_level),
195
		]);
196
	}
197
198
	/**
199
	 * Get a list of this family’s children.
200
	 *
201
	 * @param int|null $access_level
202
	 *
203
	 * @return Individual[]
204
	 */
205
	public function getChildren($access_level = null) {
206
		if ($access_level === null) {
207
			$access_level = Auth::accessLevel($this->tree);
208
		}
209
210
		$SHOW_PRIVATE_RELATIONSHIPS = $this->tree->getPreference('SHOW_PRIVATE_RELATIONSHIPS');
211
212
		$children = [];
213
		foreach ($this->getFacts('CHIL', false, $access_level, $SHOW_PRIVATE_RELATIONSHIPS) as $fact) {
0 ignored issues
show
Bug introduced by
$SHOW_PRIVATE_RELATIONSHIPS of type string is incompatible with the type boolean expected by parameter $override of Fisharebest\Webtrees\GedcomRecord::getFacts(). ( Ignorable by Annotation )

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

213
		foreach ($this->getFacts('CHIL', false, $access_level, /** @scrutinizer ignore-type */ $SHOW_PRIVATE_RELATIONSHIPS) as $fact) {
Loading history...
214
			$child = $fact->getTarget();
215
			if ($child && ($SHOW_PRIVATE_RELATIONSHIPS || $child->canShowName($access_level))) {
216
				$children[] = $child;
217
			}
218
		}
219
220
		return $children;
221
	}
222
223
	/**
224
	 * Static helper function to sort an array of families by marriage date
225
	 *
226
	 * @param Family $x
227
	 * @param Family $y
228
	 *
229
	 * @return int
230
	 */
231
	public static function compareMarrDate(Family $x, Family $y) {
232
		return Date::compare($x->getMarriageDate(), $y->getMarriageDate());
233
	}
234
235
	/**
236
	 * Number of children - for the individual list
237
	 *
238
	 * @return int
239
	 */
240
	public function getNumberOfChildren() {
241
		$nchi = count($this->getChildren());
242
		foreach ($this->getFacts('NCHI') as $fact) {
243
			$nchi = max($nchi, (int) $fact->getValue());
244
		}
245
246
		return $nchi;
247
	}
248
249
	/**
250
	 * get the marriage event
251
	 *
252
	 * @return Fact
253
	 */
254
	public function getMarriage() {
255
		return $this->getFirstFact('MARR');
256
	}
257
258
	/**
259
	 * Get marriage date
260
	 *
261
	 * @return Date
262
	 */
263
	public function getMarriageDate() {
264
		$marriage = $this->getMarriage();
265
		if ($marriage) {
0 ignored issues
show
introduced by
The condition $marriage can never be true.
Loading history...
266
			return $marriage->getDate();
267
		} else {
268
			return new Date('');
269
		}
270
	}
271
272
	/**
273
	 * Get the marriage year - displayed on lists of families
274
	 *
275
	 * @return int
276
	 */
277
	public function getMarriageYear() {
278
		return $this->getMarriageDate()->minimumDate()->y;
279
	}
280
281
	/**
282
	 * Get the marriage place
283
	 *
284
	 * @return Place
285
	 */
286
	public function getMarriagePlace() {
287
		$marriage = $this->getMarriage();
288
289
		return $marriage->getPlace();
290
	}
291
292
	/**
293
	 * Get a list of all marriage dates - for the family lists.
294
	 *
295
	 * @return Date[]
296
	 */
297
	public function getAllMarriageDates() {
298
		foreach (explode('|', WT_EVENTS_MARR) as $event) {
299
			if ($array = $this->getAllEventDates($event)) {
300
				return $array;
301
			}
302
		}
303
304
		return [];
305
	}
306
307
	/**
308
	 * Get a list of all marriage places - for the family lists.
309
	 *
310
	 * @return Place[]
311
	 */
312
	public function getAllMarriagePlaces() {
313
		foreach (explode('|', WT_EVENTS_MARR) as $event) {
314
			$places = $this->getAllEventPlaces($event);
315
			if (!empty($places)) {
316
				return $places;
317
			}
318
		}
319
320
		return [];
321
	}
322
323
	/**
324
	 * Derived classes should redefine this function, otherwise the object will have no name
325
	 *
326
	 * @return string[][]
327
	 */
328
	public function getAllNames() {
329
		if (is_null($this->_getAllNames)) {
0 ignored issues
show
introduced by
The condition is_null($this->_getAllNames) can never be true.
Loading history...
330
			// Check the script used by each name, so we can match cyrillic with cyrillic, greek with greek, etc.
331
			$husb_names = [];
332
			if ($this->husb) {
333
				$husb_names = array_filter($this->husb->getAllNames(), function (array $x) {
334
					return $x['type'] !== '_MARNM';
335
				} );
336
			}
337
			// If the individual only has married names, create a dummy birth name.
338
			if (empty($husb_names)) {
339
				$husb_names[] = [
340
					'type' => 'BIRT',
341
					'sort' => '@N.N.',
342
					'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'),
343
				];
344
			}
345
			foreach ($husb_names as $n => $husb_name) {
346
				$husb_names[$n]['script'] = I18N::textScript($husb_name['full']);
347
			}
348
349
			$wife_names = [];
350
			if ($this->wife) {
351
				$wife_names = array_filter($this->wife->getAllNames(), function (array $x) {
352
					return $x['type'] !== '_MARNM';
353
				} );
354
			}
355
			// If the individual only has married names, create a dummy birth name.
356
			if (empty($wife_names)) {
357
				$wife_names[] = [
358
					'type' => 'BIRT',
359
					'sort' => '@N.N.',
360
					'full' => I18N::translateContext('Unknown given name', '…') . ' ' . I18N::translateContext('Unknown surname', '…'),
361
				];
362
			}
363
			foreach ($wife_names as $n => $wife_name) {
364
				$wife_names[$n]['script'] = I18N::textScript($wife_name['full']);
365
			}
366
367
			// Add the matched names first
368
			foreach ($husb_names as $husb_name) {
369
				foreach ($wife_names as $wife_name) {
370
					if ($husb_name['script'] == $wife_name['script']) {
371
						$this->_getAllNames[] = [
372
							'type' => $husb_name['type'],
373
							'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'],
374
							'full' => $husb_name['full'] . ' + ' . $wife_name['full'],
375
							// No need for a fullNN entry - we do not currently store FAM names in the database
376
						];
377
					}
378
				}
379
			}
380
381
			// Add the unmatched names second (there may be no matched names)
382
			foreach ($husb_names as $husb_name) {
383
				foreach ($wife_names as $wife_name) {
384
					if ($husb_name['script'] != $wife_name['script']) {
385
						$this->_getAllNames[] = [
386
							'type' => $husb_name['type'],
387
							'sort' => $husb_name['sort'] . ' + ' . $wife_name['sort'],
388
							'full' => $husb_name['full'] . ' + ' . $wife_name['full'],
389
							// No need for a fullNN entry - we do not currently store FAM names in the database
390
						];
391
					}
392
				}
393
			}
394
		}
395
396
		return $this->_getAllNames;
397
	}
398
399
	/**
400
	 * This function should be redefined in derived classes to show any major
401
	 * identifying characteristics of this record.
402
	 *
403
	 * @return string
404
	 */
405
	public function formatListDetails() {
406
		return
407
			$this->formatFirstMajorFact(WT_EVENTS_MARR, 1) .
408
			$this->formatFirstMajorFact(WT_EVENTS_DIV, 1);
409
	}
410
}
411