Completed
Pull Request — master (#58)
by Sam
01:51
created

CategoryTraverser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 4
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
1
<?php
2
3
namespace Addwiki\MediaWikiApi\Service;
4
5
use Addwiki\MediaWikiApi\CategoryLoopException;
6
use Mediawiki\Api\MediawikiApi;
7
use Mediawiki\Api\SimpleRequest;
8
use Mediawiki\DataModel\Page;
9
use Mediawiki\DataModel\Pages;
10
use Mediawiki\DataModel\Title;
11
12
/**
13
 * Category traverser.
14
 *
15
 * Note on spelling 'descendant' (from Wiktionary):
16
 * The adjective, "descending from a biological ancestor", may be spelt either
17
 * with an 'a' or with an 'e' in the final syllable. However the noun descendant,
18
 * "one who is the progeny of someone", may be spelt only with an 'a'.
19
 */
20
class CategoryTraverser extends Service {
21
22
	const CALLBACK_CATEGORY = 10;
23
	const CALLBACK_PAGE = 20;
24
25
	/**
26
	 * @var string[]
27
	 */
28
	protected $namespaces;
29
30
	/**
31
	 * @var callable[]
32
	 */
33
	protected $callbacks;
34
35
	/**
36
	 * Used to remember the previously-visited categories when traversing.
37
	 * @var string[]
38
	 */
39
	protected $alreadyVisited;
40
41
	/**
42
	 * @param MediawikiApi $api The API to connect to.
43
	 */
44
	public function __construct( MediawikiApi $api ) {
45
		parent::__construct( $api );
46
		$this->callbacks = [];
47
	}
48
49
	/**
50
	 * Query the remote site for the list of namespaces in use, so that later we can tell what's a
51
	 * category and what's not. This populates $this->namespaces, and will not re-request on
52
	 * repeated invocations.
53
	 * @return void
54
	 */
55
	protected function retrieveNamespaces() {
56
		if ( is_array( $this->namespaces ) ) {
57
			return;
58
		}
59
		$params = [ 'meta' => 'siteinfo', 'siprop' => 'namespaces' ];
60
		$namespaces = $this->api->getRequest( new SimpleRequest( 'query', $params ) );
61
		if ( isset( $namespaces['query']['namespaces'] ) ) {
62
			$this->namespaces = $namespaces['query']['namespaces'];
63
		}
64
	}
65
66
	/**
67
	 * Register a callback that will be called for each page or category visited during the
68
	 * traversal.
69
	 * @param int $type One of the 'CALLBACK_' constants of this class.
70
	 * @param callable $callback A callable that takes two \Mediawiki\DataModel\Page parameters.
71
	 */
72
	public function addCallback( $type, $callback ) {
73
		if ( !isset( $this->callbacks[$type] ) ) {
74
			$this->callbacks[$type] = [];
75
		}
76
		$this->callbacks[$type][] = $callback;
77
	}
78
79
	/**
80
	 * Visit every descendant page of $rootCategoryName (which will be a Category
81
	 * page, because there are no desecendants of any other pages).
82
	 * @param Page $rootCat The full name of the page to start at.
83
	 * @param Page[] $currentPath Used only when recursing into this method, to track each path
84
	 * through the category hierarchy in case of loops.
85
	 * @return Pages All descendants of the given category.
86
	 * @throws CategoryLoopException If a category loop is detected.
87
	 */
88
	public function descend( Page $rootCat, $currentPath = null ) {
89
		// Make sure we know the namespace IDs.
90
		$this->retrieveNamespaces();
91
92
		$rootCatName = $rootCat->getPageIdentifier()->getTitle()->getText();
93
		if ( is_null( $currentPath ) ) {
94
			$this->alreadyVisited = [];
95
			$currentPath = new Pages();
96
		}
97
		$this->alreadyVisited[] = $rootCatName;
98
		$currentPath->addPage( $rootCat );
0 ignored issues
show
Bug introduced by
It seems like $currentPath is not always an object, but can also be of type array<integer,object<Mediawiki\DataModel\Page>>. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
99
100
		// Start a list of child pages.
101
		$descendants = new Pages();
102
		do {
103
			$pageListGetter = new PageListGetter( $this->api );
104
			$members = $pageListGetter->getPageListFromCategoryName( $rootCatName );
105
			foreach ( $members->toArray() as $member ) {
106
				/** @var Title */
107
				$memberTitle = $member->getPageIdentifier()->getTitle();
108
109
				// See if this page is a Category page.
110
				$isCat = false;
111
				if ( isset( $this->namespaces[ $memberTitle->getNs() ] ) ) {
112
					$ns = $this->namespaces[ $memberTitle->getNs() ];
113
					$isCat = ( isset( $ns['canonical'] ) && $ns['canonical'] === 'Category' );
114
				}
115
				// If it's a category, descend into it.
116
				if ( $isCat ) {
117
					// If this member has already been visited on this branch of the traversal,
118
					// throw an Exception with information about which categories form the loop.
119
					if ( $currentPath->hasPage( $member ) ) {
120
						$currentPath->addPage( $member );
121
						$loop = new CategoryLoopException();
122
						$loop->setCategoryPath( $currentPath );
0 ignored issues
show
Bug introduced by
It seems like $currentPath defined by parameter $currentPath on line 88 can also be of type array<integer,object<Mediawiki\DataModel\Page>>; however, Addwiki\MediaWikiApi\Cat...tion::setCategoryPath() does only seem to accept object<Mediawiki\DataModel\Pages>, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
123
						throw $loop;
124
					}
125
					// Don't go any further if we've already visited this member
126
					// (does not indicate a loop, however; we've already caught that above).
127
					if ( in_array( $memberTitle->getText(), $this->alreadyVisited ) ) {
128
						continue;
129
					}
130
					// Call any registered callbacked, and carry on to the next branch.
131
					$this->call( self::CALLBACK_CATEGORY, [ $member, $rootCat ] );
132
					$newDescendants = $this->descend( $member, $currentPath );
0 ignored issues
show
Bug introduced by
It seems like $currentPath can also be of type object<Mediawiki\DataModel\Pages>; however, Addwiki\MediaWikiApi\Ser...oryTraverser::descend() does only seem to accept array<integer,object<Med...i\DataModel\Page>>|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
133
					$descendants->addPages( $newDescendants );
134
					// Re-set the path.
135
					$currentPath = new Pages();
136
				} else {
137
					// If it's a page, add it to the list and carry on.
138
					$descendants->addPage( $member );
139
					$this->call( self::CALLBACK_PAGE, [ $member, $rootCat ] );
140
				}
141
			}
142
		} while ( isset( $result['continue'] ) );
143
		return $descendants;
144
	}
145
146
	/**
147
	 * Call all the registered callbacks of a particular type.
148
	 * @param int $type The callback type; should match one of the 'CALLBACK_' constants.
149
	 * @param mixed[] $params The parameters to pass to the callback function.
150
	 */
151
	protected function call( $type, $params ) {
152
		if ( !isset( $this->callbacks[$type] ) ) {
153
			return;
154
		}
155
		foreach ( $this->callbacks[$type] as $callback ) {
0 ignored issues
show
Bug introduced by
The expression $this->callbacks[$type] of type callable is not traversable.
Loading history...
156
			if ( is_callable( $callback ) ) {
157
				call_user_func_array( $callback, $params );
158
			}
159
		}
160
	}
161
162
}
163