Completed
Push — master ( 6e4b14...ad57d1 )
by mw
11s
created

PageBuilder::getResultHtml()   D

Complexity

Conditions 9
Paths 8

Size

Total Lines 41
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 19
CRAP Score 9.0698

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 9
eloc 23
c 1
b 1
f 0
nc 8
nop 0
dl 0
loc 41
ccs 19
cts 21
cp 0.9048
crap 9.0698
rs 4.909
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
		// #1728
148
		if ( !$this->pageRequestOptions->property->isValid() ) {
149
			return array( implode( ',', $this->pageRequestOptions->property->getErrors() ), '', 0 );
150
		}
151 3
152 3
		if ( $this->pageRequestOptions->valueString !== '' && !$this->pageRequestOptions->value->isValid() ) {
153
			return array( implode( ',', $this->pageRequestOptions->value->getErrors() ), '', 0 );
154 3
		}
155 2
156
		$exactResults = $this->queryResultLookup->doQuery( $this->pageRequestOptions );
157
		$exactCount = count( $exactResults );
158 2
159 1
		if ( $this->canQueryNearbyResults( $exactCount ) ) {
160
			return $this->getNearbyResults( $exactResults, $exactCount );
161 2
		}
162
163
		if ( $this->pageRequestOptions->valueString === '' ) {
164 2
			$resultMessageKey = 'smw-sp-searchbyproperty-nonvaluequery';
165
		} else {
166 2
			$resultMessageKey = 'smw-sp-searchbyproperty-valuequery';
167 2
		}
168
169 2
		$resultMessage = $this->messageBuilder->getMessage(
170 2
			$resultMessageKey,
171
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
172
			$this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
173 2
174
		if ( $exactCount > 0 ) {
175
			$resultList = $this->makeResultList( $exactResults, $this->pageRequestOptions->limit, true );
176 2
		}
177
178 2
		return array( str_replace( '_', ' ', $resultMessage ), $resultList, $exactCount );
179
	}
180 2
181 2
	private function getNearbyResults( $exactResults, $exactCount ) {
182
183 2
		$resultList = '';
184
185
		$greaterResults = $this->queryResultLookup->doQueryForNearbyResults(
186 2
			$this->pageRequestOptions,
187 2
			$exactCount,
188
			true
189 2
		);
190
191
		$smallerResults = $this->queryResultLookup->doQueryForNearbyResults(
192
			$this->pageRequestOptions,
193 2
			$exactCount,
194 2
			false
195
		);
196 2
197
		// Calculate how many greater and smaller results should be displayed
198
		$greaterCount = count( $greaterResults );
199
		$smallerCount = count( $smallerResults );
200
201
		if ( ( $greaterCount + $smallerCount + $exactCount ) > $this->pageRequestOptions->limit ) {
202
			$lhalf = round( ( $this->pageRequestOptions->limit - $exactCount ) / 2 );
203
204
			if ( $lhalf < $greaterCount ) {
205
				if ( $lhalf < $smallerCount ) {
206
					$smallerCount = $lhalf;
207
					$greaterCount = $lhalf;
208
				} else {
209
					$greaterCount = $this->pageRequestOptions->limit - ( $exactCount + $smallerCount );
210
				}
211 2
			} else {
212
				$smallerCount = $this->pageRequestOptions->limit - ( $exactCount + $greaterCount );
213
			}
214
		}
215 2
216 2
		if ( ( $greaterCount + $smallerCount + $exactCount ) == 0 ) {
217 2
			return array( '', $resultList, 0 );
218 2
		}
219
220 2
		$resultMessage = $this->messageBuilder->getMessage(
221
			'smw_sbv_displayresultfuzzy',
222 2
			$this->pageRequestOptions->property->getShortHTMLText( $this->linker ),
223
			$this->pageRequestOptions->value->getShortHTMLText( $this->linker ) )->text();
224
225
		$resultList .= $this->makeResultList( $smallerResults, $smallerCount, false );
226
227 2
		if ( $exactCount == 0 ) {
228
			$resultList .= "&#160;<em><strong><small>" . $this->messageBuilder->getMessage( 'parentheses' )
229
				->rawParams( $this->pageRequestOptions->value->getLongHTMLText() )
230 2
				->escaped() . "</small></strong></em>";
231
		} else {
232 2
			$resultList .= $this->makeResultList( $exactResults, $exactCount, true, true );
233
		}
234
235
		$resultList .= $this->makeResultList( $greaterResults, $greaterCount, true );
236
237
		return array( $resultMessage, $resultList, $greaterCount + $exactCount );
238
	}
239
240
	/**
241
	 * Creates the HTML for a bullet list with all the results of the set
242
	 * query. Values can be highlighted to show exact matches among nearby
243
	 * ones.
244
	 *
245
	 * @param array $results (array of (array of one or two SMWDataValues))
246
	 * @param integer $number How many results should be displayed? -1 for all
247
	 * @param boolean $first If less results should be displayed than
248
	 * 	given, should they show the first $number results, or the last
249 3
	 * 	$number results?
250
	 * @param boolean $highlight Should the results be highlighted?
251 3
	 *
252 3
	 * @return string  HTML with the bullet list, including header
253 3
	 */
254 3
	private function makeResultList( $results, $number, $first, $highlight = false ) {
255
256
		if ( $number > 0 ) {
257 3
			$results = $first ?
258
				array_slice( $results, 0, $number ) :
259 3
				array_slice( $results, $number );
260
		}
261 3
262 3
		$html = '';
263
264 3
		foreach ( $results as $result ) {
265
266 1
			$result[0]->setOutputFormat( 'LOCL' );
267
			$listitem = $result[0]->getLongHTMLText( $this->linker );
268 1
269 1
			if ( $this->canShowSearchByPropertyLink( $result[0] ) ) {
270 1
271
				$value = $result[0] instanceof StringValue ? $result[0]->getWikiValueForLengthOf( 72 ) : $result[0]->getWikiValue();
272 1
273 3
				$listitem .= '&#160;&#160;' . Infolink::newPropertySearchLink(
274
					'+',
275
					$this->pageRequestOptions->propertyString,
276
					$value
277 3
				)->getHTML( $this->linker );
278 3
			} elseif ( $result[0]->getTypeID() === '_wpg' ) {
279 3
280 3
				// Add browsing link for wikipage results
281
				// Note: non-wikipage results are possible using inverse properties
282
				$listitem .= '&#160;&#160;' . Infolink::newBrowsingLink(
283
					'+',
284
					$result[0]->getLongWikiText()
285 3
				)->getHTML( $this->linker );
286 3
			}
287 3
288 3
			// Show value if not equal to the value that was searched
289 3
			// or if the current results are to be highlighted:
290
			if ( array_key_exists( 1, $result ) &&
291 2
				( $result[1] instanceof DataValue ) &&
292
				( !$result[1]->getDataItem() instanceof \SMWDIError ) &&
293 2
				( !$this->pageRequestOptions->value->getDataItem()->equals( $result[1]->getDataItem() )
294 2
					|| $highlight ) ) {
295 2
296
				$result[1]->setOutputFormat( 'LOCL' );
297
298
				$listitem .= "&#160;<em><small>" . $this->messageBuilder->getMessage( 'parentheses' )
299 3
					->rawParams( $result[1]->getLongHTMLText( $this->linker ) )
300 2
					->escaped() . "</small></em>";
301
			}
302
303 3
			// Highlight values
304
			if ( $highlight ) {
305
				$listitem = "<strong>$listitem</strong>";
306 3
			}
307
308
			$html .= "<li>$listitem</li>";
309 3
		}
310 3
311
		return "<ul>$html</ul>";
312
	}
313 3
314 3
	private function canQueryNearbyResults( $exactCount ) {
315 3
		return $exactCount < ( $this->pageRequestOptions->limit / 3 ) && $this->pageRequestOptions->nearbySearch && $this->pageRequestOptions->valueString !== '';
316
	}
317
318
	private function canShowSearchByPropertyLink ( DataValue $dataValue ) {
319
		$dataTypeClass = DataTypeRegistry::getInstance()->getDataTypeClassById( $dataValue->getTypeID() );
320
		return $this->pageRequestOptions->value instanceof $dataTypeClass && $this->pageRequestOptions->valueString === '';
321
	}
322
323
	private function tryToFindAtLeastOnePropertyTableReferenceFor( DIProperty $property ) {
324
325
		$resultList = '';
326
		$resultMessage = '';
327
		$resultCount = 0;
328
		$extra = '';
329
330
		$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...
331
			$property
332
		);
333
334
		if ( !$dataItem instanceof DIWikiPage ) {
335
			$resultMessage = 'No reference found.';
336
			return array( $resultMessage, $resultList, $resultCount );
337
		}
338
339
		// In case the item has already been marked as deleted but is yet pending
340
		// for removal
341
		if ( $dataItem->getInterWiki() === ':smw-delete' ) {
342
			$resultMessage = 'Item reference "' . $dataItem->getSubobjectName() . '" has already been marked for removal.';
343
			$dataItem = new DIWikiPage( $dataItem->getDBKey(), $dataItem->getNamespace() );
344
		}
345
346
		$dataValue = DataValueFactory::getInstance()->newDataValueByItem(
347
			$dataItem
348
		);
349
350
		$dataValue->setOutputFormat( 'LOCL' );
351
352
		if ( $dataValue->isValid() ) {
353
			//$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...
354
			$resultList = $dataValue->getShortHtmlText( $this->linker ) . ' ' . $extra;
355
			$resultCount++;
356
357
			$resultList .= '&#160;&#160;' . Infolink::newBrowsingLink(
358
				'+',
359
				$dataValue->getLongWikiText()
360
			)->getHTML( $this->linker );
361
		}
362
363
		return array( $resultMessage, $resultList, $resultCount );
364
	}
365
366
}
367