CategoryTraverser::descend()   B
last analyzed

Complexity

Conditions 9
Paths 26

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 37
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 57
ccs 37
cts 37
cp 1
rs 7.3826
c 0
b 0
f 0
cc 9
nc 26
nop 2
crap 9

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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