Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
94.83% covered (success)
94.83%
110 / 116
72.22% covered (warning)
72.22%
13 / 18
CRAP
0.00% covered (danger)
0.00%
0 / 1
Person
94.83% covered (success)
94.83%
110 / 116
72.22% covered (warning)
72.22%
13 / 18
44.27
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
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%
4 / 4
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
88.89% covered (warning)
88.89%
8 / 9
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\Page\WikiPageFactory;
6use Title;
7use Wikimedia\Rdbms\ILoadBalancer;
8
9class 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}