Completed
Push — main ( 2daa48...b5d932 )
by
unknown
08:38
created

CategoryTraverser::descend()   B

Complexity

Conditions 9
Paths 26

Size

Total Lines 57

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 57
rs 7.3826
c 0
b 0
f 0
cc 9
nc 26
nop 2

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 Addwiki\Mediawiki\Api\Service;
4
5
use Addwiki\Mediawiki\Api\CategoryLoopException;
6
use Addwiki\Mediawiki\Api\Client\MediawikiApi;
7
use Addwiki\Mediawiki\Api\Client\SimpleRequest;
8
use Addwiki\Mediawiki\DataModel\Page;
9
use Addwiki\Mediawiki\DataModel\Pages;
10
use Addwiki\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
	/**
23
	 * @var int
24
	 */
25
	public const CALLBACK_CATEGORY = 10;
26
	/**
27
	 * @var int
28
	 */
29
	public const CALLBACK_PAGE = 20;
30
31
	/**
32
	 * @var string[]|null
33
	 */
34
	protected $namespaces;
35
36
	/**
37
	 * @var callable[]
38
	 */
39
	protected array $callbacks = [];
0 ignored issues
show
Bug introduced by
This code did not parse for me. Apparently, there is an error somewhere around this line:

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