Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.83% |
110 / 116 |
|
72.22% |
13 / 18 |
CRAP | |
0.00% |
0 / 1 |
Person | |
94.83% |
110 / 116 |
|
72.22% |
13 / 18 |
44.27 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
__toString | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getTitle | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getTitleHtml | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
6 | |||
getTitles | |
88.89% |
8 / 9 |
|
0.00% |
0 / 1 |
3.01 | |||
getWikiLink | |
90.91% |
20 / 22 |
|
0.00% |
0 / 1 |
10.08 | |||
hasDates | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
getBirthDate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDeathDate | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
getDateYear | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getDescription | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
getParents | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getSiblings | |
100.00% |
12 / 12 |
|
100.00% |
1 / 1 |
5 | |||
getPartners | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
2 | |||
getChildren | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
getPropInbound | |
100.00% |
15 / 15 |
|
100.00% |
1 / 1 |
2 | |||
getPropSingle | |
100.00% |
6 / 6 |
|
100.00% |
1 / 1 |
2 | |||
getPropMulti | |
95.45% |
21 / 22 |
|
0.00% |
0 / 1 |
4 |
1 | <?php |
2 | |
3 | namespace MediaWiki\Extension\Genealogy; |
4 | |
5 | use MediaWiki\Page\WikiPageFactory; |
6 | use Title; |
7 | use Wikimedia\Rdbms\ILoadBalancer; |
8 | |
9 | class Person { |
10 | |
11 | /** @var ILoadBalancer */ |
12 | private ILoadBalancer $loadBalancer; |
13 | |
14 | /** @var WikiPageFactory */ |
15 | private WikiPageFactory $wikiPageFactory; |
16 | |
17 | /** @var Title */ |
18 | private $title; |
19 | |
20 | /** @var Person[] */ |
21 | private $siblings; |
22 | |
23 | /** @var Person[] */ |
24 | private $children; |
25 | |
26 | /** |
27 | * Create a new Person based on a page in the wiki. |
28 | * @param ILoadBalancer $loadBalancer |
29 | * @param WikiPageFactory $wikiPageFactory |
30 | * @param Title $title The page title. |
31 | */ |
32 | public function __construct( |
33 | ILoadBalancer $loadBalancer, |
34 | WikiPageFactory $wikiPageFactory, |
35 | Title $title |
36 | ) { |
37 | $this->loadBalancer = $loadBalancer; |
38 | $this->wikiPageFactory = $wikiPageFactory; |
39 | $this->title = $title; |
40 | } |
41 | |
42 | /** |
43 | * Get some basic info about this person. |
44 | * @todo Add dates. |
45 | * @return string |
46 | */ |
47 | public function __toString() { |
48 | return $this->getTitle()->getPrefixedText(); |
49 | } |
50 | |
51 | /** |
52 | * Get this person's wiki title, following redirects (to any depth) when present. |
53 | * |
54 | * @return Title |
55 | */ |
56 | public function getTitle() { |
57 | $page = $this->wikiPageFactory->newFromTitle( $this->title ); |
58 | while ( $page->isRedirect() ) { |
59 | $page = $this->wikiPageFactory->newFromTitle( $page->getRedirectTarget() ); |
60 | } |
61 | return $page->getTitle(); |
62 | } |
63 | |
64 | /** |
65 | * Get the person's page title, using displaytitle if it's available. |
66 | * @return string HTML of the title. |
67 | */ |
68 | public function getTitleHtml(): string { |
69 | return $this->getPropSingle( 'displaytitle', false ) ?: $this->getTitle()->getText(); |
70 | } |
71 | |
72 | /** |
73 | * Get all Titles that refer to this Person (i.e. all redirects both inward and outward, and |
74 | * the actual Title). |
75 | * @return Title[] An array of the Titles, some of which might not actually exist, keyed by the |
76 | * prefixed DB key. |
77 | */ |
78 | public function getTitles() { |
79 | $titles = [ $this->title->getPrefixedDBkey() => $this->title ]; |
80 | // Find all the outgoing redirects that leave from here. |
81 | $page = $this->wikiPageFactory->newFromTitle( $this->title ); |
82 | while ( $page->isRedirect() ) { |
83 | $title = $page->getRedirectTarget(); |
84 | $titles[$title->getPrefixedDBkey()] = $title; |
85 | $page = $this->wikiPageFactory->newFromTitle( $title ); |
86 | } |
87 | // Find all the incoming redirects that come here. |
88 | foreach ( $this->title->getRedirectsHere() as $inwardRedirect ) { |
89 | $titles[$inwardRedirect->getPrefixedDBkey()] = $inwardRedirect; |
90 | } |
91 | return $titles; |
92 | } |
93 | |
94 | /** |
95 | * Get wikitext for a link to this Person. Non-existent people will get an 'external'-style |
96 | * link that has the 'preload' parameter added. The dates of birth and death are appended, |
97 | * outside the link. |
98 | * @return string The wikitext. |
99 | */ |
100 | public function getWikiLink(): string { |
101 | $birthYear = $this->getDateYear( $this->getBirthDate() ); |
102 | $deathYear = $this->getDateYear( $this->getDeathDate() ); |
103 | $dateString = ''; |
104 | if ( $birthYear !== '' && $deathYear !== '' ) { |
105 | $dateString = wfMessage( 'genealogy-born-and-died', $birthYear, $deathYear )->text(); |
106 | } elseif ( $birthYear !== '' && $deathYear === '' ) { |
107 | $dateString = wfMessage( 'genealogy-born', $birthYear )->text(); |
108 | } elseif ( $birthYear === '' && $deathYear !== '' ) { |
109 | $dateString = wfMessage( 'genealogy-died', $deathYear )->text(); |
110 | } |
111 | $title = $this->getTitle(); |
112 | if ( $title->exists() ) { |
113 | // If it exists, create a link (piping if not in the main namespace). |
114 | $link = $title->inNamespace( NS_MAIN ) |
115 | ? "[[" . $title->getFullText() . "]]" |
116 | : "[[" . $title->getFullText() . "|" . $title->getText() . "]]"; |
117 | } else { |
118 | // If it doesn't exist, create an edit link with a preload parameter. |
119 | $query = [ |
120 | 'action' => 'edit', |
121 | 'preload' => wfMessage( 'genealogy-person-preload' )->text(), |
122 | ]; |
123 | $url = $title->getFullURL( $query ); |
124 | $link = '[' . $url . ' ' . $title->getText() . ']'; |
125 | } |
126 | $date = ( $this->hasDates() ) ? " $dateString" : ""; |
127 | return $link . $date; |
128 | } |
129 | |
130 | /** |
131 | * Whether or not this person has a birth or death date. |
132 | * @return bool |
133 | */ |
134 | public function hasDates() { |
135 | return $this->getBirthDate() !== false || $this->getDeathDate() !== false; |
136 | } |
137 | |
138 | /** |
139 | * Get the birth date of this person. |
140 | * @return string |
141 | */ |
142 | public function getBirthDate() { |
143 | return $this->getPropSingle( 'birth date' ); |
144 | } |
145 | |
146 | /** |
147 | * Get the death date of this person. |
148 | * @return string |
149 | */ |
150 | public function getDeathDate() { |
151 | return $this->getPropSingle( 'death date' ); |
152 | } |
153 | |
154 | /** |
155 | * Get a year out of a date if possible. |
156 | * @param string $date The date to parse. |
157 | * @return string The year as a string, or the full date. |
158 | */ |
159 | public function getDateYear( string $date ): string { |
160 | preg_match( '/(\d{3,4})/', $date, $matches ); |
161 | if ( isset( $matches[1] ) ) { |
162 | return $matches[1]; |
163 | } |
164 | return $date; |
165 | } |
166 | |
167 | /** |
168 | * Get this person's description. |
169 | * @return string |
170 | */ |
171 | public function getDescription() { |
172 | $desc = $this->getPropSingle( 'description' ); |
173 | if ( !$desc ) { |
174 | $desc = ''; |
175 | } |
176 | return $desc; |
177 | } |
178 | |
179 | /** |
180 | * Get all parents. |
181 | * @return Person[] An array of parents, possibly empty. |
182 | */ |
183 | public function getParents() { |
184 | $parents = $this->getPropMulti( 'parent' ); |
185 | ksort( $parents ); |
186 | return $parents; |
187 | } |
188 | |
189 | /** |
190 | * Get all siblings. |
191 | * |
192 | * @param bool|null $excludeSelf Whether to excluding this person from the list. |
193 | * @return Person[] An array of siblings, possibly empty. |
194 | */ |
195 | public function getSiblings( ?bool $excludeSelf = false ) { |
196 | if ( !is_array( $this->siblings ) ) { |
197 | $this->siblings = []; |
198 | $descriptions = []; |
199 | foreach ( $this->getParents() as $parent ) { |
200 | foreach ( $parent->getChildren() as $child ) { |
201 | $key = $child->getTitle()->getPrefixedDBkey(); |
202 | $descriptions[ $key ] = $child->getDescription(); |
203 | $this->siblings[ $key ] = $child; |
204 | } |
205 | } |
206 | array_multisort( $descriptions, $this->siblings ); |
207 | } |
208 | if ( $excludeSelf ) { |
209 | unset( $this->siblings[ $this->getTitle()->getPrefixedDBkey() ] ); |
210 | } |
211 | return $this->siblings; |
212 | } |
213 | |
214 | /** |
215 | * Get all partners (optionally excluding those that are defined within the current page). |
216 | * @param bool $onlyDefinedElsewhere Only return those partners that are *not* defined |
217 | * within this Person's page. |
218 | * @return Person[] An array of partners, possibly empty. Keyed by the partner's page DB key. |
219 | */ |
220 | public function getPartners( $onlyDefinedElsewhere = false ) { |
221 | $partners = $this->getPropInbound( 'partner' ); |
222 | if ( $onlyDefinedElsewhere === false ) { |
223 | $partners = array_merge( $partners, $this->getPropMulti( 'partner' ) ); |
224 | } |
225 | ksort( $partners ); |
226 | return $partners; |
227 | } |
228 | |
229 | /** |
230 | * Get all children. |
231 | * @return Person[] An array of children, possibly empty, keyed by the prefixed DB key. |
232 | */ |
233 | public function getChildren() { |
234 | $this->children = $this->getPropInbound( 'parent' ); |
235 | return $this->children; |
236 | } |
237 | |
238 | /** |
239 | * Find people with properties that are equal to one of this page's titles. |
240 | * @param string $type The property type. |
241 | * @return Person[] Keyed by the prefixed DB key. |
242 | */ |
243 | protected function getPropInbound( $type ) { |
244 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
245 | $tables = [ 'pp' => 'page_props', 'p' => 'page' ]; |
246 | $columns = [ 'pp_value', 'page_title', 'page_namespace' ]; |
247 | |
248 | $where = [ |
249 | 'pp_value' => $this->getTitles(), |
250 | 'pp_propname' . $dbr->buildLike( 'genealogy ', $type . ' ', $dbr->anyString() ), |
251 | 'pp_page = page_id', |
252 | ]; |
253 | $results = $dbr->select( $tables, $columns, $where, __METHOD__, [], [ 'page' => [] ] ); |
254 | $out = []; |
255 | foreach ( $results as $res ) { |
256 | $title = Title::newFromText( $res->page_title, $res->page_namespace ); |
257 | $person = new Person( $this->loadBalancer, $this->wikiPageFactory, $title ); |
258 | $out[$person->getTitle()->getPrefixedDBkey()] = $person; |
259 | } |
260 | return $out; |
261 | } |
262 | |
263 | /** |
264 | * Get the value of a single-valued page property. |
265 | * @param string $prop The property name. |
266 | * @param bool $isGenealogy Whether the property name should be prefixed with 'genealogy '. |
267 | * @return string|bool The property value, or false if not found. |
268 | */ |
269 | public function getPropSingle( string $prop, bool $isGenealogy = true ) { |
270 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
271 | $where = [ |
272 | 'pp_page' => $this->getTitle()->getArticleID(), |
273 | 'pp_propname' => $isGenealogy ? "genealogy $prop" : $prop, |
274 | ]; |
275 | return $dbr->selectField( 'page_props', 'pp_value', $where, __METHOD__ ); |
276 | } |
277 | |
278 | /** |
279 | * Get a multi-valued relationship property of this Person. |
280 | * @param string $type The property name ('genealogy ' will be prepended). |
281 | * @return Person[] The related people. |
282 | */ |
283 | protected function getPropMulti( $type ) { |
284 | $out = []; |
285 | $dbr = $this->loadBalancer->getConnection( DB_REPLICA ); |
286 | $articleIds = []; |
287 | foreach ( $this->getTitles() as $t ) { |
288 | $articleIds[] = $t->getArticleID(); |
289 | } |
290 | $results = $dbr->select( |
291 | // Table to use. |
292 | 'page_props', |
293 | // Field to select. |
294 | 'pp_value', |
295 | [ |
296 | // Where conditions. |
297 | 'pp_page' => $articleIds, |
298 | 'pp_propname' . $dbr->buildLike( 'genealogy ', $type . ' ', $dbr->anyString() ), |
299 | ], |
300 | __METHOD__, |
301 | [ 'ORDER BY' => 'pp_value' ] |
302 | ); |
303 | foreach ( $results as $result ) { |
304 | $title = Title::newFromText( $result->pp_value ); |
305 | if ( $title === null ) { |
306 | // Do nothing, if this isn't a valid title. |
307 | continue; |
308 | } |
309 | $person = new Person( $this->loadBalancer, $this->wikiPageFactory, $title ); |
310 | $out[$person->getTitle()->getPrefixedDBkey()] = $person; |
311 | } |
312 | return $out; |
313 | } |
314 | } |