Completed
Push — master ( 17dff3...f2d821 )
by mw
42:32 queued 07:38
created

SMWQuery::getHash()   B

Complexity

Conditions 5
Paths 6

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 5

Importance

Changes 0
Metric Value
cc 5
eloc 5
nc 6
nop 0
dl 0
loc 14
ccs 5
cts 5
cp 1
crap 5
rs 8.8571
c 0
b 0
f 0
1
<?php
2
3
use SMW\DIWikiPage;
4
use SMW\HashBuilder;
5
use SMW\Query\PrintRequest;
6
use SMW\Query\Language\Description;
7
use SMW\Query\QueryContext;
8
use SMW\Query\QueryStringifier;
9
use SMW\Message;
10
11
/**
12
 * This file contains the class for representing queries in SMW, each
13
 * consisting of a query description and possible query parameters.
14
 * @ingroup SMWQuery
15
 * @author Markus Krötzsch
16
 */
17
18
/**
19
 * This group contains all parts of SMW that relate to processing semantic queries.
20
 * SMW components that relate to plain storage access (for querying or otherwise)
21
 * have their own group.
22
 * @defgroup SMWQuery SMWQuery
23
 * @ingroup SMW
24
 */
25
26
/**
27
 * Representation of queries in SMW, each consisting of a query
28
 * description and various parameters. Some settings might also lead to
29
 * changes in the query description.
30
 *
31
 * Most additional query parameters (limit, sort, ascending, ...) are
32
 * interpreted as in SMWRequestOptions (though the latter contains some
33
 * additional settings).
34
 * @ingroup SMWQuery
35
 */
36
class SMWQuery implements QueryContext {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class must be in a namespace of at least one level to avoid collisions.

You can fix this by adding a namespace to your class:

namespace YourVendor;

class YourClass { }

When choosing a vendor namespace, try to pick something that is not too generic to avoid conflicts with other libraries.

Loading history...
37
38
	const ID_PREFIX = '_QUERY';
39
40
	/**
41
	 * The time the QueryEngine required to answer a query condition
42
	 */
43
	const PROC_QUERY_TIME = 'proc.query.time';
44
45
	/**
46
	 * The time a ResultPrinter required to build the final result including all
47
	 * PrintRequests
48
	 */
49
	const PROC_PRINT_TIME = 'proc.print.time';
50
51
	/**
52
	 * The processing context in which the query is being executed
53
	 */
54
	const PROC_CONTEXT = 'proc.context';
55
56
	/**
57
	 * Suppress a possible cache request
58
	 */
59
	const NO_CACHE = 'no.cache';
60
61
	public $sort = false;
62
	public $sortkeys = array(); // format: "Property key" => "ASC" / "DESC" (note: order of entries also matters)
63
	public $querymode = self::MODE_INSTANCES;
64
65
	private $limit;
66
	private $offset = 0;
67
	private $description;
68
	private $errors = array(); // keep any errors that occurred so far
69
	private $queryString = false; // string (inline query) version (if fixed and known)
70
	private $isInline; // query used inline? (required for finding right default parameters)
71
	private $isUsedInConcept; // query used in concept? (required for finding right default parameters)
72
73
	/**
74
	 * @var PrintRequest[]
75
	 */
76
	private $m_extraprintouts = array(); // SMWPrintoutRequest objects supplied outside querystring
77
	private $m_mainlabel = ''; // Since 1.6
78
79
	/**
80
	 * @var DIWikiPage|null
81
	 */
82
	private $contextPage;
83
84
	/**
85
	 * Describes a non-local (remote) query source
86
	 *
87
	 * @var string|null
88
	 */
89
	private $querySource = null;
90
91
	/**
92
	 * @var array
93
	 */
94
	private $options = array();
95
96
	/**
97
	 * @since 1.6
98
	 *
99
	 * @param Description $description
100
	 * @param integer|boolean $context
101
	 */
102 192
	public function __construct( Description $description = null, $context = false ) {
0 ignored issues
show
Coding Style introduced by
__construct uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
103 192
		$inline = false;
104 192
		$concept = false;
105
106
		// stating whether this query runs in an inline context; used to
107
		// determine proper default parameters (e.g. the default limit)
108 192
		if ( $context === self::INLINE_QUERY ) {
109 104
			$inline = true;
110
		}
111
112
		// stating whether this query belongs to a concept; used to determine
113
		// proper default parameters (concepts usually have less restrictions)
114 192
		if ( $context === self::CONCEPT_DESC ) {
115 6
			$concept = true;
116
		}
117
118 192
		$this->limit = $inline ? $GLOBALS['smwgQMaxInlineLimit'] : $GLOBALS['smwgQMaxLimit'];
119 192
		$this->isInline = $inline;
120 192
		$this->isUsedInConcept = $concept;
121 192
		$this->description = $description;
122 192
		$this->applyRestrictions();
123 192
	}
124
125
	/**
126
	 * @since 2.5
127
	 *
128
	 * @param integer
129
	 */
130 108
	public function setQuerymode( $queryMode ) {
131 108
		$this->querymode = $queryMode;
132 108
	}
133
134
	/**
135
	 * @since 2.3
136
	 *
137
	 * @param DIWikiPage|null $contextPage
138
	 */
139 151
	public function setContextPage( DIWikiPage $contextPage = null ) {
140 151
		$this->contextPage = $contextPage;
141 151
	}
142
143
	/**
144
	 * @since 2.3
145
	 *
146
	 * @return DIWikiPage|null
147
	 */
148 145
	public function getContextPage() {
149 145
		return $this->contextPage;
150
	}
151
152
	/**
153
	 * @since 2.4
154
	 *
155
	 * @param string
156
	 */
157 108
	public function setQuerySource( $querySource ) {
158 108
		$this->querySource = $querySource;
159 108
	}
160
161
	/**
162
	 * @since 2.4
163
	 *
164
	 * @return string
165
	 */
166 93
	public function getQuerySource() {
167 93
		return $this->querySource;
168
	}
169
170
	/**
171
	 * Sets the mainlabel.
172
	 *
173
	 * @since 1.6.
174
	 *
175
	 * @param string $mainlabel
176
	 */
177 108
	public function setMainLabel( $mainlabel ) {
178 108
		$this->m_mainlabel = $mainlabel;
179 108
	}
180
181
	/**
182
	 * Gets the mainlabel.
183
	 *
184
	 * @since 1.6.
185
	 *
186
	 * @return string
187
	 */
188 13
	public function getMainLabel() {
189 13
		return $this->m_mainlabel;
190
	}
191
192 3
	public function setDescription( SMWDescription $description ) {
193 3
		$this->description = $description;
194 3
		$this->queryString = false;
195
196 3
		foreach ( $this->m_extraprintouts as $printout ) {
197
			$this->description->addPrintRequest( $printout );
198
		}
199 3
		$this->applyRestrictions();
200 3
	}
201
202 180
	public function getDescription() {
203 180
		return $this->description;
204
	}
205
206 154
	public function setExtraPrintouts( $extraprintouts ) {
207 154
		$this->m_extraprintouts = $extraprintouts;
208
209 154
		if ( !is_null( $this->description ) ) {
210 154
			foreach ( $extraprintouts as $printout ) {
211 125
				$this->description->addPrintRequest( $printout );
212
			}
213
		}
214 154
	}
215
216
	/**
217
	 * @return PrintRequest[]
218
	 */
219 122
	public function getExtraPrintouts() {
220 122
		return $this->m_extraprintouts;
221
	}
222
223 170
	public function getErrors() {
224 170
		return $this->errors;
225
	}
226
227 186
	public function addErrors( $errors ) {
228 186
		$this->errors = array_merge( $this->errors, $errors );
229 186
	}
230
231 108
	public function setQueryString( $querystring ) {
232 108
		$this->queryString = $querystring;
233 108
	}
234
235
	/**
236
	 * @since 2.5
237
	 *
238
	 * @param string|integer $key
239
	 * @param mixed $value
240
	 */
241 182
	public function setOption( $key, $value ) {
242 182
		$this->options[$key] = $value;
243 182
	}
244
245
	/**
246
	 * @since 2.5
247
	 *
248
	 * @param string|integer $key
249
	 *
250
	 * @return mixed
251
	 */
252 91
	public function getOptionBy( $key ) {
253 91
		return isset( $this->options[$key] ) ? $this->options[$key] : false;
254
	}
255
256
	/**
257
	 * @since 1.7
258
	 *
259
	 * @param  boolean $fresh
260
	 *
261
	 * @return string
262
	 */
263 122
	public function getQueryString( $fresh = false ) {
264
265
		// Mostly relevant on requesting a further results link to
266
		// ensure that localized values are transformed into a canonical
267
		// representation
268 122
		if ( $fresh && $this->description !== null ) {
269 13
			return $this->description->getQueryString();
270
		}
271
272 121
		if ( $this->queryString !== false ) {
273 95
			return $this->queryString;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->queryString; (boolean) is incompatible with the return type documented by SMWQuery::getQueryString of type string.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
274 29
		} elseif ( !is_null( $this->description ) ) {
275 29
			return $this->description->getQueryString();
276
		} else {
277
			return '';
278
		}
279
	}
280
281 173
	public function getOffset() {
282 173
		return $this->offset;
283
	}
284
285
	/**
286
	 * Set an offset for the returned query results. No offset beyond the maximal query
287
	 * limit will be set, and the current query limit might be reduced in order to ensure
288
	 * that no results beyond the maximal limit are returned.
289
	 * The function returns the chosen offset.
290
	 * @todo The function should be extended to take into account whether or not we
291
	 * are in inline mode (not critical, since offsets are usually not applicable inline).
292
	 */
293 152
	public function setOffset( $offset ) {
294 152
		global $smwgQMaxLimit;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
295 152
		$this->offset = min( $smwgQMaxLimit, $offset ); // select integer between 0 and maximal limit;
296 152
		$this->limit = min( $smwgQMaxLimit - $this->offset, $this->limit ); // note that limit might become 0 here
297 152
		return $this->offset;
298
	}
299
300 179
	public function getLimit() {
301 179
		return $this->limit;
302
	}
303
304
	/**
305
	 * Set a limit for number of query results. The set limit might be restricted by the
306
	 * current offset so as to ensure that the number of the last considered result does not
307
	 * exceed the maximum amount of supported results.
308
	 * The function returns the chosen limit.
309
	 * @note It makes sense to have limit==0, e.g. to only show a link to the search special
310
	 */
311 159
	public function setLimit( $limit, $restrictinline = true ) {
312 159
		global $smwgQMaxLimit, $smwgQMaxInlineLimit;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
313 159
		$maxlimit = ( $this->isInline && $restrictinline ) ? $smwgQMaxInlineLimit : $smwgQMaxLimit;
314 159
		$this->limit = min( $smwgQMaxLimit - $this->offset, $limit, $maxlimit );
315 159
		return $this->limit;
316
	}
317
318
	/**
319
	 * @note Sets an unbound limit that is independent from GLOBAL settings
320
	 *
321
	 * @since 2.0
322
	 *
323
	 * @param integer $limit
324
	 *
325
	 * @return Query
326
	 */
327 4
	public function setUnboundLimit( $limit ) {
328 4
		$this->limit = (int)$limit;
329 4
		return $this;
330
	}
331
332
	/**
333
	 * @note format: "Property key" => "ASC" / "DESC" (note: order of entries also matters)
334
	 *
335
	 * @since 2.2
336
	 *
337
	 * @param array $sortKeys
338
	 */
339 151
	public function setSortKeys( array $sortKeys ) {
340 151
		$this->sortkeys = $sortKeys;
341 151
	}
342
343
	/**
344
	 * @since 2.2
345
	 *
346
	 * @return array
347
	 */
348
	public function getSortKeys() {
349
		return $this->sortkeys;
350
	}
351
352
	/**
353
	 * Apply structural restrictions to the current description.
354
	 */
355 192
	public function applyRestrictions() {
356 192
		global $smwgQMaxSize, $smwgQMaxDepth, $smwgQConceptMaxSize, $smwgQConceptMaxDepth;
0 ignored issues
show
Compatibility Best Practice introduced by
Use of global functionality is not recommended; it makes your code harder to test, and less reusable.

Instead of relying on global state, we recommend one of these alternatives:

1. Pass all data via parameters

function myFunction($a, $b) {
    // Do something
}

2. Create a class that maintains your state

class MyClass {
    private $a;
    private $b;

    public function __construct($a, $b) {
        $this->a = $a;
        $this->b = $b;
    }

    public function myFunction() {
        // Do something
    }
}
Loading history...
357
358 192
		if ( !is_null( $this->description ) ) {
359 192
			if ( $this->isUsedInConcept ) {
360 6
				$maxsize = $smwgQConceptMaxSize;
361 6
				$maxdepth = $smwgQConceptMaxDepth;
362
			} else {
363 188
				$maxsize = $smwgQMaxSize;
364 188
				$maxdepth = $smwgQMaxDepth;
365
			}
366
367 192
			$log = array();
368 192
			$this->description = $this->description->prune( $maxsize, $maxdepth, $log );
369
370 192
			if ( count( $log ) > 0 ) {
371
				$this->errors[] = Message::encode( array(
372
					'smw_querytoolarge',
373
					str_replace( '[', '&#x005B;', implode( ', ', $log ) ),
374
					count( $log )
375
				) );
376
			}
377
		}
378 192
	}
379
380
	/**
381
	 * Returns serialized query details
382
	 *
383
	 * The output is following the askargs api module convention
384
	 *
385
	 * conditions The query conditions (requirements for a subject to be included)
386
	 * printouts The query printouts (which properties to show per subject)
387
	 * parameters The query parameters (non-condition and non-printout arguments)
388
	 *
389
	 * @since 1.9
390
	 *
391
	 * @return array
392
	 */
393 121
	public function toArray() {
394 121
		$serialized = array();
395
396 121
		$serialized['conditions'] = $this->getQueryString();
397
398
		// This can be extended but for the current use cases that is
399
		// sufficient since most printer related parameters have to be sourced
400
		// in the result printer class
401 121
		$serialized['parameters'] = array(
402 121
				'limit'     => $this->limit,
403 121
				'offset'    => $this->offset,
404 121
				'sortkeys'  => $this->sortkeys,
405 121
				'mainlabel' => $this->m_mainlabel,
406 121
				'querymode' => $this->querymode
407
		);
408
409
		// @2.4 Keep the queryID stable with previous versions unless
410
		// a query source is selected. The "same" query executed on different
411
		// remote systems requires a different queryID
412 121
		if ( $this->querySource !== null && $this->querySource !== '' ) {
413
			$serialized['parameters']['source'] = $this->querySource;
414
		}
415
416 121
		foreach ( $this->getExtraPrintouts() as $printout ) {
417 112
			$serialization = $printout->getSerialisation();
418 112
			if ( $serialization !== '?#' ) {
419 112
				$serialized['printouts'][] = $serialization;
420
			}
421
		}
422
423 121
		return $serialized;
424
	}
425
426
	/**
427
	 * @note Before 2.5, toArray was used to generate the content, as of 2.5
428
	 * only parameters that influence the result of an query is included.
429
	 *
430
	 * @since 2.1
431
	 *
432
	 * @return string
433
	 */
434 124
	public function getHash() {
0 ignored issues
show
Coding Style introduced by
getHash uses the super-global variable $GLOBALS which is generally not recommended.

Instead of super-globals, we recommend to explicitly inject the dependencies of your class. This makes your code less dependent on global state and it becomes generally more testable:

// Bad
class Router
{
    public function generate($path)
    {
        return $_SERVER['HOST'].$path;
    }
}

// Better
class Router
{
    private $host;

    public function __construct($host)
    {
        $this->host = $host;
    }

    public function generate($path)
    {
        return $this->host.$path;
    }
}

class Controller
{
    public function myAction(Request $request)
    {
        // Instead of
        $page = isset($_GET['page']) ? intval($_GET['page']) : 1;

        // Better (assuming you use the Symfony2 request)
        $page = $request->query->get('page', 1);
    }
}
Loading history...
435
436
		// For an optimal (less fragmentation) use of the cache, only use
437
		// elements that directly influence the result list
438 124
		$expectFingerprint = ($GLOBALS['smwgQueryResultCacheType'] !== false && $GLOBALS['smwgQueryResultCacheType'] !== CACHE_NONE ) || $GLOBALS['smwgQFilterDuplicates'] !== false;
439
440 124
		if ( $this->description !== null && $expectFingerprint ) {
441 4
			return $this->createFromFingerprint( $this->description->getFingerprint() );
442
		}
443
444
		// FIXME 3.0 Leave the hash unchanged to avoid unnecessary BC issues in
445
		// case the cache is not used.
446 120
		return HashBuilder::createFromArray( $this->toArray() );
447
	}
448
449
	/**
450
	 * @since 2.5
451
	 *
452
	 * @return string
453
	 */
454
	public function getAsString() {
455
		return QueryStringifier::get( $this );
456
	}
457
458
	/**
459
	 * @since 2.3
460
	 *
461
	 * @return string
462
	 */
463 123
	public function getQueryId() {
464 123
		return self::ID_PREFIX . $this->getHash();
465
	}
466
467 4
	private function createFromFingerprint( $fingerprint ) {
468
469 4
		$serialized = array();
470
471
		// Don't use the QueryString, use the canonized fingerprint to ensure that
472
		// [[Foo::123]][[Bar::abc]] returns the same ID as [[Bar::abc]][[Foo::123]]
0 ignored issues
show
Unused Code Comprehensibility introduced by
53% 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...
473
		// given that limit, offset, and sort/order are the same
474 4
		$serialized['fingerprint'] = $fingerprint;
475
476 4
		$serialized['parameters'] = array(
477 4
			'limit'     => $this->limit,
478 4
			'offset'    => $this->offset,
479 4
			'sortkeys'  => $this->sortkeys,
480 4
			'querymode' => $this->querymode // COUNT, DEBUG ...
481
		);
482
483
		// Make to sure to distinguish queries and results from a foreign repository
484 4
		if ( $this->querySource !== null && $this->querySource !== '' ) {
485
			$serialized['parameters']['source'] = $this->querySource;
486
		}
487
488
		// Printouts are avoided as part of the hash as they not influence the
489
		// result match process and are only resolved after the query result has
490
		// been retrieved
491 4
		return HashBuilder::createFromArray( $serialized );
492
	}
493
494
}
495