Completed
Push — master ( cfad5d...c82630 )
by adam
10s
created

CategoryTraverser::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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