Completed
Pull Request — master (#34)
by Sam
03:06
created

CategoryTraverser   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 128
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 6

Importance

Changes 0
Metric Value
wmc 18
c 0
b 0
f 0
lcom 1
cbo 6
dl 0
loc 128
rs 10

5 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A retrieveNamespaces() 0 10 3
A addCallback() 0 6 2
C descend() 0 43 8
A call() 0 10 4
1
<?php
2
3
namespace Mediawiki\Api\Service;
4
5
use Mediawiki\Api\MediawikiApi;
6
use Mediawiki\Api\SimpleRequest;
7
use Mediawiki\DataModel\PageIdentifier;
8
use Mediawiki\DataModel\Pages;
9
10
/**
11
 * Category traverser.
12
 *
13
 * Note on spelling 'descendant' (from Wiktionary):
14
 * The adjective, "descending from a biological ancestor", may be spelt either
15
 * with an 'a' or with an 'e' in the final syllable. However the noun descendant,
16
 * "one who is the progeny of someone", may be spelt only with an 'a'.
17
 */
18
class CategoryTraverser {
19
20
	const CALLBACK_CATEGORY = 10;
21
	const CALLBACK_PAGE = 20;
22
23
	/**
24
	 * @var \Mediawiki\Api\MediawikiApi
25
	 */
26
	protected $api;
27
28
	/**
29
	 * @var string[]
30
	 */
31
	protected $namespaces;
32
33
	/**
34
	 * @var callable[]
35
	 */
36
	protected $callbacks;
37
38
	/**
39
	 * Used to remember the previously-visited categories when traversing.
40
	 * @var string[]
41
	 */
42
	protected $alreadyVisited;
43
44
	public function __construct( MediawikiApi $api ) {
45
		$this->api = $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 integer $type One of the 'CALLBACK_' constants of this class.
70
	 * @param callable $callback The callable that takes two 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 PageIdentifier $rootCat The full name of the page to start at.
83
	 * @return Pages All descendants of the given category.
84
	 */
85
	public function descend( PageIdentifier $rootCat, $recursing = false ) {
86
		// Make sure we know namespace IDs.
87
		$this->retrieveNamespaces();
88
89
		$rootCatName = $rootCat->getTitle()->getText();
90
		if ( $recursing === false ) {
91
		    $this->alreadyVisited = [];
92
		}
93
		$this->alreadyVisited[] = $rootCatName;
94
95
		// Start a list of child pages.
96
		$descendants = new Pages();
97
		do {
98
		    $pageListGetter = new PageListGetter( $this->api );
99
			$members = $pageListGetter->getPageListFromCategoryName( $rootCatName );
100
			foreach ( $members->toArray() as $member ) {
101
			    /** @var Title */
102
			    $memberIdent = $member->getPageIdentifier();
103
				$memberTitle = $memberIdent->getTitle();
104
105
				// See if this page is a Category page.
106
				$isCat = false;
107
				if ( isset( $this->namespaces[ $memberTitle->getNs() ] ) ) {
108
					$ns = $this->namespaces[ $memberTitle->getNs() ];
109
					$isCat = ( isset( $ns['canonical'] ) && $ns['canonical'] === 'Category' );
110
				}
111
				if ( $isCat ) {
112
				    // If it's a category, descend into it (if we haven't already).
113
					if ( in_array( $memberTitle->getText(), $this->alreadyVisited ) ) {
114
						continue;
115
					}
116
					$this->call( self::CALLBACK_CATEGORY, [ $member, $rootCat ] );
117
					$newDescendants = $this->descend( $memberIdent, true );
118
					$descendants->addPages( $newDescendants );
119
				} else {
120
				    // If it's a page, add it to the list and carry on.
121
					$descendants->addPage( $member );
122
					$this->call( self::CALLBACK_PAGE, [ $member, $rootCat ] );
123
				}
124
			}
125
		} while ( isset( $result['continue'] ) );
126
		return $descendants;
127
	}
128
129
	/**
130
	 * Call all the registered callbacks of a particular type.
131
	 * @param integer $type The callback type; should match one of the 'CALLBACK_' constants.
132
	 * @param mixed[] $params The parameters to pass to the callback function.
133
	 */
134
	protected function call( $type, $params ) {
135
		if ( !isset( $this->callbacks[$type] ) ) {
136
			return;
137
		}
138
		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...
139
			if ( is_callable( $callback ) ) {
140
				call_user_func_array( $callback, $params );
141
			}
142
		}
143
	}
144
145
}
146