Completed
Push — master ( ec2f81...11966d )
by adam
03:28
created

CategoryTraverser   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 148
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 19
lcom 1
cbo 7
dl 0
loc 148
ccs 0
cts 72
cp 0
rs 10
c 0
b 0
f 0

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A retrieveNamespaces() 0 10 3
A addCallback() 0 6 2
B descend() 0 57 9
A call() 0 10 4
1
<?php
2
3
namespace Mediawiki\Api\Service;
4
5
use Mediawiki\Api\CategoryLoopException;
6
use Mediawiki\Api\MediawikiApi;
7
use Mediawiki\Api\SimpleRequest;
8
use Mediawiki\DataModel\Page;
9
use Mediawiki\DataModel\Pages;
10
11
/**
12
 * Category traverser.
13
 *
14
 * Note on spelling 'descendant' (from Wiktionary):
15
 * The adjective, "descending from a biological ancestor", may be spelt either
16
 * with an 'a' or with an 'e' in the final syllable. However the noun descendant,
17
 * "one who is the progeny of someone", may be spelt only with an 'a'.
18
 */
19
class CategoryTraverser {
20
21
	const CALLBACK_CATEGORY = 10;
22
	const CALLBACK_PAGE = 20;
23
24
	/**
25
	 * @var \Mediawiki\Api\MediawikiApi
26
	 */
27
	protected $api;
28
29
	/**
30
	 * @var string[]
31
	 */
32
	protected $namespaces;
33
34
	/**
35
	 * @var callable[]
36
	 */
37
	protected $callbacks;
38
39
	/**
40
	 * Used to remember the previously-visited categories when traversing.
41
	 * @var string[]
42
	 */
43
	protected $alreadyVisited;
44
45
	/**
46
	 * @param MediawikiApi $api The API to connect to.
47
	 */
48
	public function __construct( MediawikiApi $api ) {
49
		$this->api = $api;
50
		$this->callbacks = [];
51
	}
52
53
	/**
54
	 * Query the remote site for the list of namespaces in use, so that later we can tell what's a
55
	 * category and what's not. This populates $this->namespaces, and will not re-request on
56
	 * repeated invocations.
57
	 * @return void
58
	 */
59
	protected function retrieveNamespaces() {
60
		if ( is_array( $this->namespaces ) ) {
61
			return;
62
		}
63
		$params = [ 'meta' => 'siteinfo', 'siprop' => 'namespaces' ];
64
		$namespaces = $this->api->getRequest( new SimpleRequest( 'query', $params ) );
65
		if ( isset( $namespaces['query']['namespaces'] ) ) {
66
			$this->namespaces = $namespaces['query']['namespaces'];
67
		}
68
	}
69
70
	/**
71
	 * Register a callback that will be called for each page or category visited during the
72
	 * traversal.
73
	 * @param int $type One of the 'CALLBACK_' constants of this class.
74
	 * @param callable $callback A callable that takes two \Mediawiki\DataModel\Page parameters.
75
	 */
76
	public function addCallback( $type, $callback ) {
77
		if ( !isset( $this->callbacks[$type] ) ) {
78
			$this->callbacks[$type] = [];
79
		}
80
		$this->callbacks[$type][] = $callback;
81
	}
82
83
	/**
84
	 * Visit every descendant page of $rootCategoryName (which will be a Category
85
	 * page, because there are no desecendants of any other pages).
86
	 * @param Page $rootCat The full name of the page to start at.
87
	 * @param Page[] $currentPath Used only when recursing into this method, to track each path
88
	 * through the category hierarchy in case of loops.
89
	 * @return Pages All descendants of the given category.
90
	 * @throws CategoryLoopException If a category loop is detected.
91
	 */
92
	public function descend( Page $rootCat, $currentPath = null ) {
93
		// Make sure we know the namespace IDs.
94
		$this->retrieveNamespaces();
95
96
		$rootCatName = $rootCat->getPageIdentifier()->getTitle()->getText();
97
		if ( is_null( $currentPath ) ) {
98
			$this->alreadyVisited = [];
99
			$currentPath = new Pages();
100
		}
101
		$this->alreadyVisited[] = $rootCatName;
102
		$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...
103
104
		// Start a list of child pages.
105
		$descendants = new Pages();
106
		do {
107
			$pageListGetter = new PageListGetter( $this->api );
108
			$members = $pageListGetter->getPageListFromCategoryName( $rootCatName );
109
			foreach ( $members->toArray() as $member ) {
110
				/** @var Title */
111
				$memberTitle = $member->getPageIdentifier()->getTitle();
112
113
				// See if this page is a Category page.
114
				$isCat = false;
115
				if ( isset( $this->namespaces[ $memberTitle->getNs() ] ) ) {
116
					$ns = $this->namespaces[ $memberTitle->getNs() ];
117
					$isCat = ( isset( $ns['canonical'] ) && $ns['canonical'] === 'Category' );
118
				}
119
				// If it's a category, descend into it.
120
				if ( $isCat ) {
121
					// If this member has already been visited on this branch of the traversal,
122
					// throw an Exception with information about which categories form the loop.
123
					if ( $currentPath->hasPage( $member ) ) {
124
						$currentPath->addPage( $member );
125
						$loop = new CategoryLoopException();
126
						$loop->setCategoryPath( $currentPath );
0 ignored issues
show
Bug introduced by
It seems like $currentPath defined by parameter $currentPath on line 92 can also be of type array<integer,object<Mediawiki\DataModel\Page>>; however, Mediawiki\Api\CategoryLo...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...
127
						throw $loop;
128
					}
129
					// Don't go any further if we've already visited this member
130
					// (does not indicate a loop, however; we've already caught that above).
131
					if ( in_array( $memberTitle->getText(), $this->alreadyVisited ) ) {
132
						continue;
133
					}
134
					// Call any registered callbacked, and carry on to the next branch.
135
					$this->call( self::CALLBACK_CATEGORY, [ $member, $rootCat ] );
136
					$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, Mediawiki\Api\Service\CategoryTraverser::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...
137
					$descendants->addPages( $newDescendants );
138
					// Re-set the path.
139
					$currentPath = new Pages();
140
				} else {
141
					// If it's a page, add it to the list and carry on.
142
					$descendants->addPage( $member );
143
					$this->call( self::CALLBACK_PAGE, [ $member, $rootCat ] );
144
				}
145
			}
146
		} while ( isset( $result['continue'] ) );
147
		return $descendants;
148
	}
149
150
	/**
151
	 * Call all the registered callbacks of a particular type.
152
	 * @param int $type The callback type; should match one of the 'CALLBACK_' constants.
153
	 * @param mixed[] $params The parameters to pass to the callback function.
154
	 */
155
	protected function call( $type, $params ) {
156
		if ( !isset( $this->callbacks[$type] ) ) {
157
			return;
158
		}
159
		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...
160
			if ( is_callable( $callback ) ) {
161
				call_user_func_array( $callback, $params );
162
			}
163
		}
164
	}
165
166
}
167