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