Completed
Push — master ( 30add5...f043e7 )
by mw
14s
created

PageBuilder::getHtml()   C

Complexity

Conditions 7
Paths 4

Size

Total Lines 33
Code Lines 20

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 7.1653

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 33
ccs 17
cts 20
cp 0.85
rs 6.7272
cc 7
eloc 20
nc 4
nop 0
crap 7.1653
1
<?php
2
3
namespace SMW\MediaWiki\Specials\SearchByProperty;
4
5
use Html;
6
use SMW\ApplicationFactory;
7
use SMW\DataTypeRegistry;
8
use SMW\DataValueFactory;
9
use SMW\DIProperty;
10
use SMW\DIWikiPage;
11
use SMW\MediaWiki\MessageBuilder;
12
use SMW\MediaWiki\Renderer\HtmlFormRenderer;
13
use SMWDataValue as DataValue;
14
use SMWInfolink as Infolink;
15
use SMWStringValue as StringValue;
16
17
/**
18
 * @license GNU GPL v2+
19
 * @since   2.1
20
 *
21
 * @author Denny Vrandecic
22
 * @author Daniel Herzig
23
 * @author Markus Kroetzsch
24
 * @author mwjames
25
 */
26
class PageBuilder {
27
28
	/**
29
	 * @var HtmlFormRenderer
30
	 */
31
	private $htmlFormRenderer;
32
33
	/**
34
	 * @var PageRequestOptions
35
	 */
36
	private $pageRequestOptions;
37
38
	/**
39
	 * @var QueryResultLookup
40
	 */
41
	private $queryResultLookup;
42
43
	/**
44
	 * @var MessageBuilder
45
	 */
46
	private $messageBuilder;
47
48
	/**
49
	 * @var Linker
50
	 */
51
	private $linker;
52
53
	/**
54
	 * @since 2.1
55
	 *
56
	 * @param HtmlFormRenderer $htmlFormRenderer
57
	 * @param PageRequestOptions $pageRequestOptions
58
	 * @param QueryResultLookup $queryResultLookup
59
	 */
60 4
	public function __construct( HtmlFormRenderer $htmlFormRenderer, PageRequestOptions $pageRequestOptions, QueryResultLookup $queryResultLookup ) {
61 4
		$this->htmlFormRenderer = $htmlFormRenderer;
62 4
		$this->pageRequestOptions = $pageRequestOptions;
63 4
		$this->queryResultLookup = $queryResultLookup;
64 4
		$this->linker = smwfGetLinker();
0 ignored issues
show
Documentation Bug introduced by
It seems like smwfGetLinker() of type object<Linker> is incompatible with the declared type object<SMW\MediaWiki\Spe...earchByProperty\Linker> of property $linker.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
65 4
	}
66
67
	/**
68
	 * @since 2.1
69
	 *
70
	 * @return string
71
	 */
72 3
	public function getHtml() {
73
74 3
		$this->pageRequestOptions->initialize();
75 3
		$this->messageBuilder = $this->htmlFormRenderer->getMessageBuilder();
76
77 3
		list( $resultMessage, $resultList, $resultCount ) = $this->getResultHtml();
78
79 3
		if ( ( $resultList === '' || $resultList === null ) &&
80 3
			$this->pageRequestOptions->property->getDataItem() instanceof DIProperty &&
81 3
			$this->pageRequestOptions->valueString === '' ) {
82
			list( $resultMessage, $resultList, $resultCount ) = $this->tryToFindAtLeastOnePropertyTableReferenceFor(
83
				$this->pageRequestOptions->property->getDataItem()
84
			);
85
		}
86
87 3
		if ( $resultList === '' || $resultList === null ) {
88
			$resultList = $this->messageBuilder->getMessage( 'smw_result_noresults' )->text();
89
		}
90
91 3
		$pageDescription = Html::rawElement(
92 3
			'p',
93 3
			array( 'class' => 'smw-sp-searchbyproperty-description' ),
94 3
			$this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-description' )->parse()
95
		);
96
97 3
		$resultListHeader = Html::element(
98 3
			'h2',
99 3
			array(),
100 3
			$this->messageBuilder->getMessage( 'smw-sp-searchbyproperty-resultlist-header' )->text()
101
		);
102
103 3
		return $pageDescription . $this->getHtmlForm( $resultMessage, $resultCount ) . $resultListHeader . $resultList;
104
	}
105
106 3
	private function getHtmlForm( $resultMessage, $resultCount ) {
107
108
		// Precaution to avoid any inline breakage caused by a div element
109
		// within a paragraph (e.g Highlighter content)
110 3
		$resultMessage = str_replace( 'div', 'span', $resultMessage );
111
112 3
		$html = $this->htmlFormRenderer
113 3
			->setName( 'searchbyproperty' )
114 3
			->withFieldset()
115 3
			->addParagraph( $resultMessage )
116 3
			->addPaging(
117 3
				$this->pageRequestOptions->limit,
118 3
				$this->pageRequestOptions->offset,
119
				$resultCount )
120 3
			->addHorizontalRule()
121 3
			->addInputField(
122 3
				$this->messageBuilder->getMessage( 'smw_sbv_property' )->text(),
123 3
				'property',
124 3
				$this->pageRequestOptions->propertyString,
125 3
				'smw-property-input' )
126 3
			->addNonBreakingSpace()
127 3
			->addInputField(
128 3
				$this->messageBuilder->getMessage( 'smw_sbv_value' )->text(),
129 3
				'value',
130 3
				$this->pageRequestOptions->valueString,
131 3
				'smw-value-input' )
132 3
			->addNonBreakingSpace()
133 3
			->addSubmitButton( $this->messageBuilder->getMessage( 'smw_sbv_submit' )->text() )
134 3
			->getForm();
135
136 3
		return $html;
137
	}
138
139 3
	private function getResultHtml() {
140
141 3
		$resultList = '';
142
143 3
		if ( $this->pageRequestOptions->propertyString === '' || !$this->pageRequestOptions->propertyString ) {
144
			return array( $this->messageBuilder->getMessage( 'smw_sbv_docu' )->text(), '', 0 );
145
		}
146
147 3
		if ( $this->pageRequestOptions->valueString !== '' && !$this->pageRequestOptions->value->isValid() ) {
148
			return array( implode( ',', $this->pageRequestOptions->value->getErrors() ), '', 0 );
149
		}
150
151 3
		$exactResults = $this->queryResultLookup->doQuery( $this->pageRequestOptions );
152 3
		$exactCount = count( $exactResults );
153
154 3
		if ( $this->canQueryNearbyResults( $exactCount ) ) {
155 2
			return $this->getNearbyResults( $exactResults, $exactCount );
156
		}
157
158 2
		if ( $this->pageRequestOptions->valueString === '' ) {
159 1
			$resultMessageKey = 'smw-sp-searchbyproperty-nonvaluequery';
160
		} else {
161 1
			$resultMessageKey = 'smw-sp-searchbyproperty-valuequery';
162
		}
163
164 2
		$resultMessage = $this->messageBuilder->getMessage(
165
			$resultMessageKey,
166 2
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
167 2
			$this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
168
169 2
		if ( $exactCount > 0 ) {
170 2
			$resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
171
		}
172
173 2
		return array( str_replace( '_', ' ', $resultMessage ), $resultList, $exactCount );
174
	}
175
176 2
	private function getNearbyResults( $exactResults, $exactCount ) {
177
178 2
		$resultList = '';
179
180 2
		$greaterResults = $this->queryResultLookup->doQueryForNearbyResults(
181 2
			$this->pageRequestOptions,
182
			$exactCount,
183 2
			true
184
		);
185
186 2
		$smallerResults = $this->queryResultLookup->doQueryForNearbyResults(
187 2
			$this->pageRequestOptions,
188
			$exactCount,
189 2
			false
190
		);
191
192
		// Calculate how many greater and smaller results should be displayed
193 2
		$greaterCount = count( $greaterResults );
194 2
		$smallerCount = count( $smallerResults );
195
196 2
		if ( ( $greaterCount + $smallerCount + $exactCount ) > $this->pageRequestOptions->limit ) {
197
			$lhalf = round( ( $this->pageRequestOptions->limit - $exactCount ) / 2 );
198
199
			if ( $lhalf < $greaterCount ) {
200
				if ( $lhalf < $smallerCount ) {
201
					$smallerCount = $lhalf;
202
					$greaterCount = $lhalf;
203
				} else {
204
					$greaterCount = $this->pageRequestOptions->limit - ( $exactCount + $smallerCount );
205
				}
206
			} else {
207
				$smallerCount = $this->pageRequestOptions->limit - ( $exactCount + $greaterCount );
208
			}
209
		}
210
211 2
		if ( ( $greaterCount + $smallerCount + $exactCount ) == 0 ) {
212
			return array( '', $resultList, 0 );
213
		}
214
215 2
		$resultMessage = $this->messageBuilder->getMessage(
216 2
			'smw_sbv_displayresultfuzzy',
217 2
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
218 2
			$this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
219
220 2
		$resultList .= $this->makeResultList( $smallerResults, $smallerCount, false );
221
222 2
		if ( $exactCount == 0 ) {
223
			$resultList .= "&#160;<em><strong><small>" . $this->messageBuilder->getMessage( 'parentheses' )
224
				->rawParams( $this->pageRequestOptions->value->getLongHTMLText() )
225
				->escaped() . "</small></strong></em>";
226
		} else {
227 2
			$resultList .= $this->makeResultList( $exactResults, $exactCount, true, true );
228
		}
229
230 2
		$resultList .= $this->makeResultList( $greaterResults, $greaterCount, true );
231
232 2
		return array( $resultMessage, $resultList, $greaterCount + $exactCount );
233
	}
234
235
	/**
236
	 * Creates the HTML for a bullet list with all the results of the set
237
	 * query. Values can be highlighted to show exact matches among nearby
238
	 * ones.
239
	 *
240
	 * @param array $results (array of (array of one or two SMWDataValues))
241
	 * @param integer $number How many results should be displayed? -1 for all
242
	 * @param boolean $first If less results should be displayed than
243
	 * 	given, should they show the first $number results, or the last
244
	 * 	$number results?
245
	 * @param boolean $highlight Should the results be highlighted?
246
	 *
247
	 * @return string  HTML with the bullet list, including header
248
	 */
249 3
	private function makeResultList( $results, $number, $first, $highlight = false ) {
250
251 3
		if ( $number > 0 ) {
252 3
			$results = $first ?
253 3
				array_slice( $results, 0, $number ) :
254 3
				array_slice( $results, $number );
255
		}
256
257 3
		$html = '';
258
259 3
		foreach ( $results as $result ) {
260 3
			$listitem = $result[0]->getLongHTMLText( $this->linker );
261
262 3
			if ( $this->canShowSearchByPropertyLink( $result[0] ) ) {
263
264 1
				$value = $result[0] instanceof StringValue ? $result[0]->getWikiValueForLengthOf( 72 ) : $result[0]->getWikiValue();
265
266 1
				$listitem .= '&#160;&#160;' . Infolink::newPropertySearchLink(
267 1
					'+',
268 1
					$this->pageRequestOptions->propertyString,
269
					$value
270 1
				)->getHTML( $this->linker );
271 3
			} elseif ( $result[0]->getTypeID() === '_wpg' ) {
272
273
				// Add browsing link for wikipage results
274
				// Note: non-wikipage results are possible using inverse properties
275 3
				$listitem .= '&#160;&#160;' . Infolink::newBrowsingLink(
276 3
					'+',
277 3
					$result[0]->getLongWikiText()
278 3
				)->getHTML( $this->linker );
279
			}
280
281
			// Show value if not equal to the value that was searched
282
			// or if the current results are to be highlighted:
283 3
			if ( array_key_exists( 1, $result ) &&
284 3
				( $result[1] instanceof DataValue ) &&
285 3
				( !$result[1]->getDataItem() instanceof \SMWDIError ) &&
286 3
				( !$this->pageRequestOptions->value->getDataItem()->equals( $result[1]->getDataItem() )
287 3
					|| $highlight ) ) {
288
289 2
				$listitem .= "&#160;<em><small>" . $this->messageBuilder->getMessage( 'parentheses' )
290 2
					->rawParams( $result[1]->getLongHTMLText( $this->linker ) )
291 2
					->escaped() . "</small></em>";
292
			}
293
294
			// Highlight values
295 3
			if ( $highlight ) {
296 2
				$listitem = "<strong>$listitem</strong>";
297
			}
298
299 3
			$html .= "<li>$listitem</li>";
300
		}
301
302 3
		return "<ul>$html</ul>";
303
	}
304
305 3
	private function canQueryNearbyResults( $exactCount ) {
306 3
		return $exactCount < ( $this->pageRequestOptions->limit / 3 ) && $this->pageRequestOptions->nearbySearch && $this->pageRequestOptions->valueString !== '';
307
	}
308
309 3
	private function canShowSearchByPropertyLink ( DataValue $dataValue ) {
310 3
		$dataTypeClass = DataTypeRegistry::getInstance()->getDataTypeClassById( $dataValue->getTypeID() );
311 3
		return $this->pageRequestOptions->value instanceof $dataTypeClass && $this->pageRequestOptions->valueString === '';
312
	}
313
314
	private function tryToFindAtLeastOnePropertyTableReferenceFor( DIProperty $property ) {
315
316
		$resultList = '';
317
		$resultMessage = '';
318
		$resultCount = 0;
319
		$extra = '';
320
321
		$dataItem = ApplicationFactory::getInstance()->getStore()->getPropertyTableIdReferenceFinder()->tryToFindAtLeastOneReferenceForProperty(
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMW\Store as the method getPropertyTableIdReferenceFinder() does only exist in the following sub-classes of SMW\Store: SMWSQLStore3, SMWSparqlStore, SMW\SPARQLStore\SPARQLStore, SMW\Tests\Utils\Mock\FakeQueryStore. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
322
			$property
323
		);
324
325
		if ( !$dataItem instanceof DIWikiPage ) {
326
			$resultMessage = 'No reference found.';
327
			return array( $resultMessage, $resultList, $resultCount );
328
		}
329
330
		// In case the item has already been marked as deleted but is yet pending
331
		// for removal
332
		if ( $dataItem->getInterWiki() === ':smw-delete' ) {
333
			$resultMessage = 'Item reference "' . $dataItem->getSubobjectName() . '" has already been marked for removal.';
334
			$dataItem = new DIWikiPage( $dataItem->getDBKey(), $dataItem->getNamespace() );
335
		}
336
337
		$dataValue = DataValueFactory::getInstance()->newDataItemValue(
338
			$dataItem
339
		);
340
341
		if ( $dataValue->isValid() ) {
342
			//$resultMessage = 'Item reference for a zero-marked property.';
0 ignored issues
show
Unused Code Comprehensibility introduced by
50% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
343
			$resultList = $dataValue->getShortHtmlText( $this->linker ) . ' ' . $extra;
344
			$resultCount++;
345
346
			$resultList .= '&#160;&#160;' . Infolink::newBrowsingLink(
347
				'+',
348
				$dataValue->getLongWikiText()
349
			)->getHTML( $this->linker );
350
		}
351
352
		return array( $resultMessage, $resultList, $resultCount );
353
	}
354
355
}
356