Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
76.47% covered (warning)
76.47%
13 / 17
CRAP
94.29% covered (success)
94.29%
99 / 105
Person
0.00% covered (danger)
0.00%
0 / 1
76.47% covered (warning)
76.47%
13 / 17
42.33
94.29% covered (success)
94.29%
99 / 105
 __construct
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 __toString
0.00% covered (danger)
0.00%
0 / 1
2
0.00% covered (danger)
0.00%
0 / 1
 getTitle
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getTitles
0.00% covered (danger)
0.00%
0 / 1
3.01
88.89% covered (warning)
88.89%
8 / 9
 getWikiLink
0.00% covered (danger)
0.00%
0 / 1
10.34
85.00% covered (warning)
85.00%
17 / 20
 hasDates
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
1 / 1
 getBirthDate
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDeathDate
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
1 / 1
 getDateYear
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getDescription
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
4 / 4
 getParents
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
3 / 3
 getSiblings
100.00% covered (success)
100.00%
1 / 1
6
100.00% covered (success)
100.00%
12 / 12
 getPartners
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
5 / 5
 getChildren
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
2 / 2
 getPropInbound
100.00% covered (success)
100.00%
1 / 1
2
100.00% covered (success)
100.00%
13 / 13
 getPropSingle
100.00% covered (success)
100.00%
1 / 1
1
100.00% covered (success)
100.00%
4 / 4
 getPropMulti
0.00% covered (danger)
0.00%
0 / 1
4.00
94.74% covered (success)
94.74%
18 / 19
<?php
namespace MediaWiki\Extension\Genealogy;
use Title;
use WikiPage;
class Person {
    /** @var Title */
    private $title;
    /** @var Person[] */
    private $siblings;
    /** @var Person[] */
    private $children;
    /**
     * Create a new Person based on a page in the wiki.
     * @param Title $title The page title.
     */
    public function __construct( Title $title ) {
        $this->title = $title;
    }
    /**
     * Get some basic info about this person.
     * @todo Add dates.
     * @return string
     */
    public function __toString() {
        return $this->getTitle()->getPrefixedText();
    }
    /**
     * Get this person's wiki title, following redirects (to any depth) when present.
     *
     * @return Title
     */
    public function getTitle() {
        $page = WikiPage::factory( $this->title );
        while ( $page->isRedirect() ) {
            $page = WikiPage::factory( $page->getRedirectTarget() );
        }
        return $page->getTitle();
    }
    /**
     * Get all Titles that refer to this Person (i.e. all redirects both inward and outward, and
     * the actual Title).
     * @return Title[] An array of the Titles, some of which might not actually exist, keyed by the
     * prefixed DB key.
     */
    public function getTitles() {
        $titles = [ $this->title->getPrefixedDBkey() => $this->title ];
        // Find all the outgoing redirects that leave from here.
        $page = WikiPage::factory( $this->title );
        while ( $page->isRedirect() ) {
            $title = $page->getRedirectTarget();
            $titles[$title->getPrefixedDBkey()] = $title;
            $page = WikiPage::factory( $title );
        }
        // Find all the incoming redirects that come here.
        foreach ( $this->title->getRedirectsHere() as $inwardRedirect ) {
            $titles[$inwardRedirect->getPrefixedDBkey()] = $inwardRedirect;
        }
        return $titles;
    }
    /**
     * Get wikitext for a link to this Person. Non-existent people will get an 'external'-style
     * link that has the 'preload' parameter added. The dates of birth and death are appended,
     * outside the link.
     * @return string The wikitext.
     */
    public function getWikiLink() {
        $birthYear = $this->getDateYear( $this->getBirthDate() );
        $deathYear = $this->getDateYear( $this->getDeathDate() );
        $dateString = '';
        if ( !empty( $birthYear ) && !empty( $deathYear ) ) {
            $dateString = wfMessage( 'genealogy-born-and-died', $birthYear, $deathYear )->text();
        } elseif ( !empty( $birthYear ) && empty( $deathYear ) ) {
            $dateString = wfMessage( 'genealogy-born', $birthYear )->text();
        } elseif ( empty( $birthYear ) && !empty( $deathYear ) ) {
            $dateString = wfMessage( 'genealogy-died', $deathYear )->text();
        }
        $title = $this->getTitle();
        if ( $title->exists() ) {
            // If it exists, create a link (piping if not in the main namespace).
            $link = $title->inNamespace( NS_MAIN )
                ? "[[" . $title->getFullText() . "]]"
                : "[[" . $title->getFullText() . "|" . $title->getText() . "]]";
        } else {
            // If it doesn't exist, create an edit link with a preload parameter.
            $query = [
                'action' => 'edit',
                'preload' => wfMessage( 'genealogy-person-preload' )->text(),
            ];
            $url = $title->getFullURL( $query );
            $link = '[' . $url . ' ' . $title->getText() . ']';
        }
        $date = ( $this->hasDates() ) ? " $dateString" : "";
        return $link . $date;
    }
    /**
     * Whether or not this person has a birth or death date.
     * @return bool
     */
    public function hasDates() {
        return $this->getBirthDate() !== false || $this->getDeathDate() !== false;
    }
    /**
     * Get the birth date of this person.
     * @return string
     */
    public function getBirthDate() {
        return $this->getPropSingle( 'birth date' );
    }
    /**
     * Get the death date of this person.
     * @return string
     */
    public function getDeathDate() {
        return $this->getPropSingle( 'death date' );
    }
    /**
     * Get a year out of a date if possible.
     * @param string $date The date to parse.
     * @return string The year as a string, or the full date.
     */
    public function getDateYear( $date ) {
        preg_match( '/(\d{3,4})/', $date, $matches );
        if ( isset( $matches[1] ) ) {
            return $matches[1];
        }
        return $date;
    }
    /**
     * Get this person's description.
     * @return string
     */
    public function getDescription() {
        $desc = $this->getPropSingle( 'description' );
        if ( !$desc ) {
            $desc = '';
        }
        return $desc;
    }
    /**
     * Get all parents.
     * @return Person[] An array of parents, possibly empty.
     */
    public function getParents() {
        $parents = $this->getPropMulti( 'parent' );
        ksort( $parents );
        return $parents;
    }
    /**
     * Get all siblings.
     *
     * @param bool|null $excludeSelf Whether to excluding this person from the list.
     * @return Person[] An array of siblings, possibly empty.
     */
    public function getSiblings( ?bool $excludeSelf = false ) {
        if ( !is_array( $this->siblings ) ) {
            $this->siblings = [];
            $descriptions = [];
            foreach ( $this->getParents() as $parent ) {
                foreach ( $parent->getChildren() as $child ) {
                    $key = $child->getTitle()->getPrefixedDBkey();
                    $descriptions[ $key ] = $child->getDescription();
                    $this->siblings[ $key ] = $child;
                }
            }
            array_multisort( $descriptions, $this->siblings );
        }
        if ( $excludeSelf ) {
            unset( $this->siblings[ $this->getTitle()->getPrefixedDBkey() ] );
        }
        return $this->siblings;
    }
    /**
     * Get all partners (optionally excluding those that are defined within the current page).
     * @param bool $onlyDefinedElsewhere Only return those partners that are *not* defined
     * within this Person's page.
     * @return Person[] An array of partners, possibly empty. Keyed by the partner's page DB key.
     */
    public function getPartners( $onlyDefinedElsewhere = false ) {
        $partners = $this->getPropInbound( 'partner' );
        if ( $onlyDefinedElsewhere === false ) {
            $partners = array_merge( $partners, $this->getPropMulti( 'partner' ) );
        }
        ksort( $partners );
        return $partners;
    }
    /**
     * Get all children.
     * @return Person[] An array of children, possibly empty, keyed by the prefixed DB key.
     */
    public function getChildren() {
        $this->children = $this->getPropInbound( 'parent' );
        return $this->children;
    }
    /**
     * Find people with properties that are equal to one of this page's titles.
     * @param string $type The property type.
     * @return Person[] Keyed by the prefixed DB key.
     */
    protected function getPropInbound( $type ) {
        $dbr = wfGetDB( DB_REPLICA );
        $tables = [ 'pp' => 'page_props', 'p' => 'page' ];
        $columns = [ 'pp_value', 'page_title', 'page_namespace' ];
        $where = [
            'pp_value' => $this->getTitles(),
            'pp_propname' . $dbr->buildLike( 'genealogy ', $type . ' ', $dbr->anyString() ),
            'pp_page = page_id',
        ];
        $results = $dbr->select( $tables, $columns, $where, __METHOD__, [], [ 'page' => [] ] );
        $out = [];
        foreach ( $results as $res ) {
            $title = Title::newFromText( $res->page_title, $res->page_namespace );
            $person = new Person( $title );
            $out[$person->getTitle()->getPrefixedDBkey()] = $person;
        }
        return $out;
    }
    /**
     * Get the value of a single-valued page property.
     * @param string $prop The property.
     * @return string|bool The property value, or false if not found.
     */
    public function getPropSingle( $prop ) {
        $dbr = wfGetDB( DB_REPLICA );
        $where = [
            'pp_page' => $this->getTitle()->getArticleID(),
            'pp_propname' => "genealogy $prop"
        ];
        return $dbr->selectField( 'page_props', 'pp_value', $where, __METHOD__ );
    }
    /**
     * Get a multi-valued relationship property of this Person.
     * @param string $type The property name ('genealogy ' will be prepended).
     * @return Person[] The related people.
     */
    protected function getPropMulti( $type ) {
        $out = [];
        $dbr = wfGetDB( DB_REPLICA );
        $articleIds = [];
        foreach ( $this->getTitles() as $t ) {
            $articleIds[] = $t->getArticleID();
        }
        $results = $dbr->select(
            // Table to use.
            'page_props',
            // Field to select.
            'pp_value',
            [
                // Where conditions.
                'pp_page' => $articleIds,
                'pp_propname' . $dbr->buildLike( 'genealogy ', $type . ' ', $dbr->anyString() ),
            ],
            __METHOD__,
            [ 'ORDER BY' => 'pp_value' ]
        );
        foreach ( $results as $result ) {
            $title = Title::newFromText( $result->pp_value );
            if ( $title === null ) {
                // Do nothing, if this isn't a valid title.
                continue;
            }
            $person = new Person( $title );
            $out[$person->getTitle()->getPrefixedDBkey()] = $person;
        }
        return $out;
    }
}