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