SMWSQLStore3Readers::fetchSemanticData()   F
last analyzed

Complexity

Conditions 34
Paths > 20000

Size

Total Lines 133
Code Lines 77

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 69
CRAP Score 34.0259

Importance

Changes 0
Metric Value
cc 34
eloc 77
nc 31945
nop 5
dl 0
loc 133
ccs 69
cts 71
cp 0.9718
crap 34.0259
rs 2
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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
use SMW\DataTypeRegistry;
4
use SMW\DIWikiPage;
5
use SMW\DIProperty;
6
use SMW\SQLStore\TableDefinition;
7
use SMW\SQLStore\EntityStore\Exception\DataItemHandlerException;
8
use SMW\SQLStore\TableBuilder\FieldType;
9
10
/**
11
 * Class to provide all basic read methods for SMWSQLStore3.
12
 *
13
 * @author Markus Krötzsch
14
 * @author Jeroen De Dauw
15
 * @author Nischay Nahata
16
 *
17
 * @since 1.8
18
 * @ingroup SMWStore
19
 */
20
class SMWSQLStore3Readers {
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...
21
22
	/**
23
	 * The store used by this store reader
24
	 *
25
	 * @since 1.8
26
	 * @var SMWSQLStore3
27
	 */
28
	private $store;
29
30
	/**
31
	 * @var SQLStoreFactory
32
	 */
33
	private $factory;
34
35
	/**
36
	 *  >0 while getSemanticData runs, used to prevent nested calls from clearing the cache
37
	 * while another call runs and is about to fill it with data
38 5
	 *
39 5
	 * @var int
40 5
	 */
41
	private static $in_getSemanticData = 0;
42
43
	public function __construct( SMWSQLStore3 $parentStore, $factory ) {
44
		$this->store = $parentStore;
45
		$this->factory = $factory;
46
	}
47
48
	/**
49 271
	 * @see SMWStore::getSemanticData()
50
	 * @since 1.8
51
	 *
52 271
	 * @param DIWikiPage $subject
53
	 * @param string[]|bool $filter
54 271
	 */
55 271
	public function getSemanticData( DIWikiPage $subject, $filter = false ) {
56 271
57 271
		// *** Find out if this subject exists ***//
58 271
		$sortKey = '';
59
60 271
		$sid = $this->store->smwIds->getSMWPageIDandSort(
61 271
			$subject->getDBkey(),
62
			$subject->getNamespace(),
63
			$subject->getInterwiki(),
64
			$subject->getSubobjectName(),
65
			$sortKey,
66
			true,
67 271
			true
68 267
		);
69
70
		// Ensures that a cached item to contain an expected sortKey when
71 271
		// for example the ID was just created and the sortKey from the DB
72
		// is empty otherwise the DB wins over the invoked sortKey
73 271
		if ( !$sortKey ) {
74
			$sortKey = $subject->getSortKey();
75
		}
76 267
77
		$subject->setSortKey( $sortKey );
78
79 262
		if ( $sid == 0 ) {
80
			// We consider redirects for getting $sid,
81 262
			// so $sid == 0 also means "no redirects".
82 262
			return new SMWSemanticData( $subject );
83 262
		}
84
85
		$propertyTableHashes = $this->store->smwIds->getPropertyTableHashes( $sid );
86 253
87 3
		foreach ( $this->store->getPropertyTables() as $tid => $proptable ) {
0 ignored issues
show
Bug introduced by
The expression $this->store->getPropertyTables() of type null|array<integer,objec...Store\TableDefinition>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
88 3
			if ( !array_key_exists( $proptable->getName(), $propertyTableHashes ) ) {
89 3
				continue;
90 3
			}
91 3
92 3
			if ( $filter !== false ) {
93
				$relevant = false;
94
				foreach ( $filter as $typeId ) {
0 ignored issues
show
Bug introduced by
The expression $filter of type array<integer,string>|boolean is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
95 3
					$diType = DataTypeRegistry::getInstance()->getDataItemId( $typeId );
0 ignored issues
show
Deprecated Code introduced by
The method SMW\DataTypeRegistry::getDataItemId() has been deprecated with message: since 2.5, use DataTypeRegistry::getDataItemByType

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
96 3
					$relevant = $relevant || ( $proptable->getDiType() == $diType );
97
					if ( $relevant ) {
98
						break;
99
					}
100 253
				}
101
				if ( !$relevant ) {
102
					continue;
103
				}
104
			}
105 262
106 262
			$this->getSemanticDataFromTable( $sid, $subject, $proptable );
107
		}
108
109 262
		// Note: the sortkey is always set but belongs to no property table,
110 200
		// hence no entry in $this->store->m_sdstate[$sid] is made.
111
		self::$in_getSemanticData++;
112
		$this->initSemanticDataCache( $sid, $subject );
113 262
114
		// Avoid adding a sortkey for an already extended stub
115 262
		if ( !$this->store->m_semdata[$sid]->hasProperty( new DIProperty( '_SKEY' ) ) ) {
116
			$this->store->m_semdata[$sid]->addPropertyStubValue( '_SKEY', array( '', $sortKey ) );
117
		}
118
119
		self::$in_getSemanticData--;
120
121
		return $this->store->m_semdata[$sid];
122
	}
123
124
	/**
125
	 * Helper method to make sure there is a cache entry for the data about
126
	 * the given subject with the given ID.
127
	 *
128
	 * @todo The management of this cache should be revisited.
129 265
	 *
130
	 * @since 1.8
131
	 *
132 265
	 * @param int $subjectId
133 179
	 * @param DIWikiPage $subject
134 179
	 */
135
	private function initSemanticDataCache( $subjectId, DIWikiPage $subject ) {
136
137
		// *** Prepare the cache ***//
138
		if ( !array_key_exists( $subjectId, $this->store->m_semdata ) ) { // new cache entry
139
			$this->store->m_semdata[$subjectId] = new SMWSql3StubSemanticData( $subject, $this->store, false );
0 ignored issues
show
Compatibility introduced by
$subject of type object<SMW\DIWikiPage> is not a sub-type of object<SMWDIWikiPage>. It seems like you assume a child class of the class SMW\DIWikiPage to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
140
			$this->store->m_sdstate[$subjectId] = array();
141 265
		}
142 25
143 25
		// Issue #622
144
		// If a redirect was cached preceding this request and points to the same
145
		// subject id ensure that in all cases the requested subject matches with
146 265
		// the selected DB id
147
		if ( $this->store->m_semdata[$subjectId]->getSubject()->getHash() !== $subject->getHash() ) {
148
			$this->store->m_semdata[$subjectId] = new SMWSql3StubSemanticData( $subject, $this->store, false );
0 ignored issues
show
Compatibility introduced by
$subject of type object<SMW\DIWikiPage> is not a sub-type of object<SMWDIWikiPage>. It seems like you assume a child class of the class SMW\DIWikiPage to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
149
			$this->store->m_sdstate[$subjectId] = array();
150
		}
151 32
152 32
		if ( ( count( $this->store->m_semdata ) > 20 ) && ( self::$in_getSemanticData == 1 ) ) {
153
			// prevent memory leak;
154 265
			// It is not so easy to find the sweet spot between cache size and performance gains (both memory and time),
155
			// The value of 20 was chosen by profiling runtimes for large inline queries and heavily annotated pages.
156
			// However, things might have changed in the meantime ...
157
			$this->store->m_semdata = array( $subjectId => $this->store->m_semdata[$subjectId] );
158
			$this->store->m_sdstate = array( $subjectId => $this->store->m_sdstate[$subjectId] );
159
		}
160
	}
161
162
	/**
163
	 * Fetch the data storder about one subject in one particular table.
164
	 *
165 256
	 * @param integer $sid
166
	 * @param DIWikiPage $subject
167 256
	 * @param TableDefinition $proptable
168
	 *
169 256
	 * @return SMWSemanticData
170
	 */
171 256
	private function getSemanticDataFromTable( $sid, DIWikiPage $subject, TableDefinition $proptable ) {
172 253
		// Do not clear the cache when called recursively.
173 253
		self::$in_getSemanticData++;
174
175
		$this->initSemanticDataCache( $sid, $subject );
176
177 132
		if ( array_key_exists( $proptable->getName(), $this->store->m_sdstate[$sid] ) ) {
178 132
			self::$in_getSemanticData--;
179 75
			return $this->store->m_semdata[$sid];
180
		}
181 132
182
		// *** Read the data ***//
183 132
		$data = $this->fetchSemanticData( $sid, $subject, $proptable );
184 132
		foreach ( $data as $d ) {
185
			$this->store->m_semdata[$sid]->addPropertyStubValue( reset( $d ), end( $d ) );
186
		}
187
		$this->store->m_sdstate[$sid][$proptable->getName()] = true;
188
189
		self::$in_getSemanticData--;
190
		return $this->store->m_semdata[$sid];
191
	}
192
193
	/**
194
	 * @see SMWStore::getPropertyValues
195
	 *
196
	 * @todo Retrieving all sortkeys (values for _SKEY with $subject null)
197
	 * is not supported. An empty array will be given.
198
	 *
199
	 * @since 1.8
200
	 *
201 230
	 * @param $subject mixed DIWikiPage or null
202
	 * @param $property SMWDIProperty
203 230
	 * @param $requestOptions SMWRequestOptions
204 4
	 *
205 4
	 * @return SMWDataItem[]
206 230
	 */
207 230
	public function getPropertyValues( $subject, SMWDIProperty $property, $requestOptions = null ) {
208 230
209 230
		if ( $property->isInverse() ) { // inverses are working differently
210 230
			$noninverse = new SMW\DIProperty( $property->getKey(), false );
211 181
			$result = $this->getPropertySubjects( $noninverse, $subject, $requestOptions );
0 ignored issues
show
Compatibility introduced by
$noninverse of type object<SMW\DIProperty> is not a sub-type of object<SMWDIProperty>. It seems like you assume a child class of the class SMW\DIProperty to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
212 202
		} elseif ( !is_null( $subject ) ) { // subject given, use semantic data cache
213 7
			$sid = $this->store->smwIds->getSMWPageID( $subject->getDBkey(),
214 7
				$subject->getNamespace(), $subject->getInterwiki(),
215 7
				$subject->getSubobjectName(), true );
216 7
			if ( $sid == 0 ) {
217 7
				$result = array();
218
			} elseif ( $property->getKey() == '_SKEY' ) {
219 202
				$this->store->smwIds->getSMWPageIDandSort( $subject->getDBkey(),
220 202
				$subject->getNamespace(), $subject->getInterwiki(),
221
				$subject->getSubobjectName(), $sortKey, true );
222 202
				$sortKeyDi = new SMWDIBlob( $sortKey );
223
				$result = $this->store->applyRequestOptions( array( $sortKeyDi ), $requestOptions );
224
			} else {
225
				$propTableId = $this->store->findPropertyTableID( $property );
226 202
				$proptables =  $this->store->getPropertyTables();
227 230
228
				if ( !isset( $proptables[$propTableId] ) ) {
229
					return array();
230 2
				}
231 2
232
				$sd = $this->getSemanticDataFromTable( $sid, $subject, $proptables[$propTableId] );
233 2
				$result = $this->store->applyRequestOptions( $sd->getPropertyValues( $property ), $requestOptions );
234
			}
235
		} else { // no subject given, get all values for the given property
236
			$pid = $this->store->smwIds->getSMWPropertyID( $property );
237 2
			$tableid =  $this->store->findPropertyTableID( $property );
238 2
239 2
			if ( ( $pid == 0 ) || ( $tableid === '' ) ) {
240 2
				return array();
241 2
			}
242
243 2
			$proptables =  $this->store->getPropertyTables();
244
			$data = $this->fetchSemanticData( $pid, $property, $proptables[$tableid], false, $requestOptions );
245 2
			$result = array();
246 2
			$propertyTypeId = $property->findPropertyTypeID();
247 2
			$propertyDiId = DataTypeRegistry::getInstance()->getDataItemId( $propertyTypeId );
0 ignored issues
show
Deprecated Code introduced by
The method SMW\DataTypeRegistry::getDataItemId() has been deprecated with message: since 2.5, use DataTypeRegistry::getDataItemByType

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
248
249
			foreach ( $data as $dbkeys ) {
250
				try {
251
					$diHandler = $this->store->getDataItemHandlerForDIType( $propertyDiId );
252
					$result[] = $diHandler->dataItemFromDBKeys( $dbkeys );
253
				} catch ( SMWDataItemException $e ) {
254
					// maybe type assignment changed since data was stored;
255 230
					// don't worry, but we can only drop the data here
256
				}
257
			}
258
		}
259
260
261
		return $result;
262
	}
263
264
	/**
265
	 * Helper function for reading all data for from a given property table
266
	 * (specified by an SMWSQLStore3Table object), based on certain
267
	 * restrictions. The function can filter data based on the subject (1)
268
	 * or on the property it belongs to (2) -- but one of those must be
269
	 * done. The Boolean $issubject is true for (1) and false for (2).
270
	 *
271
	 * In case (1), the first two parameters are taken to refer to a
272
	 * subject; in case (2) they are taken to refer to a property. In any
273
	 * case, the retrieval is limited to the specified $proptable. The
274
	 * parameters are an internal $id (of a subject or property), and an
275
	 * $object (being an DIWikiPage or SMWDIProperty). Moreover, when
276
	 * filtering by property, it is assumed that the given $proptable
277
	 * belongs to the property: if it is a table with fixed property, it
278
	 * will not be checked that this is the same property as the one that
279
	 * was given in $object.
280
	 *
281
	 * In case (1), the result in general is an array of pairs (arrays of
282
	 * size 2) consisting of a property key (string), and DB keys (array if
283
	 * many, string if one) from which a datvalue object for this value can
284
	 * be built. It is possible that some of the DB keys are based on
285
	 * internal objects; these will be represented by similar result arrays
286
	 * of (recursive calls of) fetchSemanticData().
287
	 *
288
	 * In case (2), the result is simply an array of DB keys (array)
289
	 * without the property keys. Container objects will be encoded with
290
	 * nested arrays like in case (1).
291
	 *
292
	 * @todo Maybe share DB handler; asking for it seems to take quite some
293
	 * time and we do not want to change it in one call.
294
	 *
295
	 * @param integer $id
296
	 * @param SMWDataItem $object
297 133
	 * @param TableDefinition $propTable
298
	 * @param boolean $isSubject
299
	 * @param SMWRequestOptions $requestOptions
300
	 *
301 133
	 * @return array
302 133
	 */
303
	private function fetchSemanticData( $id, SMWDataItem $object = null, TableDefinition $propTable, $isSubject = true, SMWRequestOptions $requestOptions = null ) {
304
		// stop if there is not enough data:
305
		// properties always need to be given as object,
306 133
		// subjects at least if !$proptable->idsubject
307 133
		if ( ( $id == 0 ) ||
308
			( is_null( $object ) && ( !$isSubject || !$propTable->usesIdSubject() ) ) ) {
309 133
				return array();
310
		}
311
312 133
		$result = array();
313
		$db = $this->store->getConnection();
314 133
315 133
		$diHandler = $this->store->getDataItemHandlerForDIType( $propTable->getDiType() );
316
317 133
		// ***  First build $from, $select, and $where for the DB query  ***//
318 132
		$from = $db->tableName( $propTable->getName() ); // always use actual table
319 33
320 132
		$select = '';
321 132
		$where  = '';
322 74
323 132
		if ( $isSubject ) { // restrict subject, select property
324
			$where .= ( $propTable->usesIdSubject() ) ? 's_id=' . $db->addQuotes( $id ) :
325 2
					  's_title=' . $db->addQuotes( $object->getDBkey() ) .
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMWDataItem as the method getDBkey() does only exist in the following sub-classes of SMWDataItem: SMWDIWikiPage, SMW\DIWikiPage. 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...
326 2
					  ' AND s_namespace=' . $db->addQuotes( $object->getNamespace() );
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class SMWDataItem as the method getNamespace() does only exist in the following sub-classes of SMWDataItem: SMWDIWikiPage, SMW\DIWikiPage. 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...
327
			if ( !$propTable->isFixedPropertyTable() ) { // select property name
328
				$from .= ' INNER JOIN ' . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . ' AS p ON p_id=p.smw_id';
329 133
				$select .= 'p.smw_title as prop';
330
			} // else: fixed property, no select needed
331 133
		} elseif ( !$propTable->isFixedPropertyTable() ) { // restrict property only
332
			$where .= 'p_id=' . $db->addQuotes( $id );
333 133
		}
334 133
335 133
		$valuecount = 0;
336 133
		// Don't use DISTINCT for value of one subject:
337 133
		$usedistinct = !$isSubject;
338 106
339 106
		$valueField = $diHandler->getIndexField();
340 106
		$labelField = $diHandler->getLabelField();
341 106
		$fields = $diHandler->getFetchFields();
342 106
		foreach ( $fields as $fieldname => $fieldType ) { // select object column(s)
343 106
			if ( $fieldType === FieldType::FIELD_ID ) { // get data from ID table
344 106
				$from .= ' INNER JOIN ' . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " AS o$valuecount ON $fieldname=o$valuecount.smw_id";
345 106
				$select .= ( ( $select !== '' ) ? ',' : '' ) .
346
					"$fieldname AS id$valuecount" .
347 106
					",o$valuecount.smw_title AS v$valuecount" .
348 106
					",o$valuecount.smw_namespace AS v" . ( $valuecount + 1 ) .
349
					",o$valuecount.smw_iw AS v" . ( $valuecount + 2 ) .
350 106
					",o$valuecount.smw_sortkey AS v" . ( $valuecount + 3 ) .
351 106
					",o$valuecount.smw_subobject AS v" . ( $valuecount + 4 );
352
353
				if ( $valueField == $fieldname ) {
354 106
					$valueField = "o$valuecount.smw_sortkey";
355
				}
356 111
				if ( $labelField == $fieldname ) {
357 111
					$labelField = "o$valuecount.smw_sortkey";
358
				}
359
360 133
				$valuecount += 4;
361
			} else {
362
				$select .= ( ( $select !== '' ) ? ',' : '' ) .
363
					"$fieldname AS v$valuecount";
364
			}
365
366 133
			$valuecount += 1;
367 71
		}
368
369
		// Postgres
370 133
		// Function: SMWSQLStore3Readers::fetchSemanticData
371 2
		// Error: 42P10 ERROR: for SELECT DISTINCT, ORDER BY expressions must appear in select list
372
		if ( strpos( $select, $valueField ) === false ) {
373 132
			$select .= ", $valueField AS v" . ( $valuecount + 1 );
374
		}
375
376
		if ( !$isSubject ) { // Apply sorting/string matching; only with given property
377 133
			$where .= $this->store->getSQLConditions( $requestOptions, $valueField, $labelField, $where !== '' );
378 133
		} else {
379 2
			$valueField = '';
380 133
		}
381
382
		// ***  Now execute the query and read the results  ***//
383 133
		$res = $db->select( $from, $select, $where, __METHOD__,
384
				( $usedistinct ?
385 77
					$this->store->getSQLOptions( $requestOptions, $valueField ) + array( 'DISTINCT' ) :
386
					$this->store->getSQLOptions( $requestOptions, $valueField )
387 77
				) );
388 75
389 75
		foreach ( $res as $row ) {
390
391
			$valueHash = '';
392
393 77
			if ( $isSubject ) { // use joined or predefined property name
394 73
				$propertykey = $propTable->isFixedPropertyTable() ? $propTable->getFixedProperty() : $row->prop;
395 73
				$valueHash = $propertykey;
396 73
			}
397 73
398
			// Use enclosing array only for results with many values:
399
			if ( $valuecount > 1 ) {
400 69
				$valuekeys = array();
401
				for ( $i = 0; $i < $valuecount; $i += 1 ) { // read the value fields from the current row
402
					$fieldname = "v$i";
403
					$valuekeys[] = $row->$fieldname;
404
				}
405 77
			} else {
406 8
				$valuekeys = $row->v0;
407
			}
408
409
			// #Issue 615
410 77
			// If the iw field contains a redirect marker then remove it
411
			if ( isset( $valuekeys[2] ) && ( $valuekeys[2] === SMW_SQL3_SMWREDIIW || $valuekeys[2] === SMW_SQL3_SMWDELETEIW ) ) {
412
				$valuekeys[2] = '';
413 77
			}
414 60
415 60
			// The valueHash prevents from inserting duplicate entries of the same content
416 77
			$valueHash = $valuecount > 1 ? md5( $valueHash . implode( '#', $valuekeys ) ) : md5( $valueHash . $valuekeys );
417
418 77
			// Filter out any accidentally retrieved internal things (interwiki starts with ":"):
419
			if ( $valuecount < 3 ||
420
				implode( '', $fields ) !== FieldType::FIELD_ID ||
421
				$valuekeys[2] === '' ||
422 77
				$valuekeys[2]{0} != ':' ) {
423
424
				if ( isset( $result[$valueHash] ) ) {
425
					$this->factory->getLogger()->info( __METHOD__ . " Duplicate entry for {$propertykey} with " . ( is_array( $valuekeys ) ? implode( ',', $valuekeys ) : $valuekeys ) . "\n" );
0 ignored issues
show
Bug introduced by
The variable $propertykey does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
426 133
				}
427
428 133
				$result[$valueHash] = $isSubject ? array( $propertykey, $valuekeys ) : $valuekeys;
429
			}
430
		}
431
432
		$db->freeResult( $res );
433
434
		return $result;
435
	}
436
437
	/**
438
	 * @see SMWStore::getPropertySubjects
439
	 *
440
	 * @todo This method cannot retrieve subjects for sortkeys, i.e., for
441
	 * property _SKEY. Only empty arrays will be returned there.
442
	 *
443 176
	 * @param SMWDIProperty $property
444
	 * @param SMWDataItem|null $value
445
	 * @param SMWRequestOptions|null $requestOptions
446 176
	 *
447
	 * @return array of DIWikiPage
448
	 */
449
	public function getPropertySubjects( SMWDIProperty $property, SMWDataItem $value = null, SMWRequestOptions $requestOptions = null ) {
450
		/// TODO: should we share code with #ask query computation here? Just use queries?
451
452
		if ( $property->isInverse() ) { // inverses are working differently
453
			$noninverse = new SMW\DIProperty( $property->getKey(), false );
454 176
			$result = $this->getPropertyValues( $value, $noninverse, $requestOptions );
0 ignored issues
show
Compatibility introduced by
$noninverse of type object<SMW\DIProperty> is not a sub-type of object<SMWDIProperty>. It seems like you assume a child class of the class SMW\DIProperty to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
455 1
			return $result;
456
		}
457
458
		// #1222, Filter those where types don't match (e.g property = _txt
459 176
		// and value = _wpg)
460 176
		if ( $value !== null && DataTypeRegistry::getInstance()->getDataItemId( $property->findPropertyTypeID() ) !== $value->getDIType() ) {
0 ignored issues
show
Deprecated Code introduced by
The method SMW\DataTypeRegistry::getDataItemId() has been deprecated with message: since 2.5, use DataTypeRegistry::getDataItemByType

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
461 176
			return array();
462
		}
463 176
464 162
		// First build $select, $from, and $where for the DB query
465
		$where = $from = '';
0 ignored issues
show
Unused Code introduced by
$from is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
466
		$pid = $this->store->smwIds->getSMWPropertyID( $property );
467 174
		$tableid =  $this->store->findPropertyTableID( $property );
468 174
469 174
		if ( ( $pid == 0 ) || ( $tableid === '' ) ) {
470
			return array();
471 174
		}
472 174
473 174
		$proptables =  $this->store->getPropertyTables();
474
		$proptable = $proptables[$tableid];
475 4
		$db = $this->store->getConnection();
476 4
477
		if ( $proptable->usesIdSubject() ) { // join with ID table to get title data
478
			$from = $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " INNER JOIN " . $db->tableName( $proptable->getName() ) . " AS t1 ON t1.s_id=smw_id";
479 174
			$select = 'smw_title, smw_namespace, smw_iw, smw_sortkey, smw_subobject';
480 166
		} else { // no join needed, title+namespace as given in proptable
481
			$from = $db->tableName( $proptable->getName() ) . " AS t1";
482
			$select = 's_title AS smw_title, s_namespace AS smw_namespace, \'\' AS smw_iw, s_title AS smw_sortkey, \'\' AS smw_subobject';
483 174
		}
484
485
		if ( !$proptable->isFixedPropertyTable() ) {
486 174
			$where .= ( $where ? ' AND ' : '' ) . "t1.p_id=" . $db->addQuotes( $pid );
487
		}
488 174
489 166
		$this->prepareValueQuery( $from, $where, $proptable, $value, 1 );
0 ignored issues
show
Bug introduced by
It seems like $value defined by parameter $value on line 449 can be null; however, SMWSQLStore3Readers::prepareValueQuery() does not accept null, maybe add an additional type check?

It seems like you allow that null is being passed for a parameter, however the function which is called does not seem to accept null.

We recommend to add an additional type check (or disallow null for the parameter):

function notNullable(stdClass $x) { }

// Unsafe
function withoutCheck(stdClass $x = null) {
    notNullable($x);
}

// Safe - Alternative 1: Adding Additional Type-Check
function withCheck(stdClass $x = null) {
    if ($x instanceof stdClass) {
        notNullable($x);
    }
}

// Safe - Alternative 2: Changing Parameter
function withNonNullableParam(stdClass $x) {
    notNullable($x);
}
Loading history...
490 166
491
		// ***  Now execute the query and read the results  ***//
492
		$result = array();
493
494
		if ( !$proptable->isFixedPropertyTable() ) {
495
			if ( $where !== '' && strpos( SMW_SQL3_SMWIW_OUTDATED, $where ) === false ) {
496 174
				$where .= " AND smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWDELETEIW );
497 174
			} else {
498 174
				$where .= " smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWDELETEIW );
499
			}
500 174
		}
501
502 174
		$res = $db->select( $from, 'DISTINCT ' . $select,
503
		                    $where . $this->store->getSQLConditions( $requestOptions, 'smw_sortkey', 'smw_sortkey', $where !== '' ),
504 17
		                    __METHOD__, $this->store->getSQLOptions( $requestOptions, 'smw_sortkey' ) );
505 17
506
		$diHandler = $this->store->getDataItemHandlerForDIType( SMWDataItem::TYPE_WIKIPAGE );
507 17
508
		foreach ( $res as $row ) {
509
			try {
510
				if ( $row->smw_iw === '' || $row->smw_iw{0} != ':' ) { // filter special objects
511
					$result[] = $diHandler->dataItemFromDBKeys( array_values( (array)$row ) );
512 174
				}
513
			} catch ( DataItemHandlerException $e ) {
514 174
				// silently drop data, should be extremely rare and will usually fix itself at next edit
515
			}
516
		}
517
518
		$db->freeResult( $res );
519
520
		return $result;
521
	}
522
523
524
	/**
525
	 * Helper function to compute from and where strings for a DB query so that
526
	 * only rows of the given value object match. The parameter $tableindex
527
	 * counts that tables used in the query to avoid duplicate table names. The
528
	 * parameter $proptable provides the SMWSQLStore3Table object that is
529
	 * queried.
530
	 *
531
	 * @todo Maybe do something about redirects. The old code was
532
	 * $oid = $this->store->smwIds->getSMWPageID($value->getDBkey(),$value->getNamespace(),$value->getInterwiki(),false);
533
	 *
534
	 * @note This method cannot handle DIContainer objects with sortkey
535
	 * properties correctly. This should never occur, but it would be good
536
	 * to fail in a more controlled way if it ever does.
537
	 *
538 177
	 * @param string $from
539 177
	 * @param string $where
540
	 * @param TableDefinition $propTable
541 177
	 * @param SMWDataItem $value
542
	 * @param integer $tableIndex
543
	 */
544
	private function prepareValueQuery( &$from, &$where, TableDefinition $propTable, $value, $tableIndex = 1 ) {
545
		$db = $this->store->getConnection();
546
547
		if ( $value instanceof SMWDIContainer ) { // recursive handling of containers
548
			$keys = array_keys( $propTable->getFields( $this->store ) );
549
			$joinfield = "t$tableIndex." . reset( $keys ); // this must be a type 'p' object
550
			$proptables =  $this->store->getPropertyTables();
551
			$semanticData = $value->getSemanticData();
552
553
			foreach ( $semanticData->getProperties() as $subproperty ) {
554
				$tableid =  $this->store->findPropertyTableID( $subproperty );
555
				$subproptable = $proptables[$tableid];
556
557
				foreach ( $semanticData->getPropertyValues( $subproperty ) as $subvalue ) {
558
					$tableIndex++;
559
560
					if ( $subproptable->usesIdSubject() ) { // simply add property table to check values
561
						$from .= " INNER JOIN " . $db->tableName( $subproptable->getName() ) . " AS t$tableIndex ON t$tableIndex.s_id=$joinfield";
562
					} else { // exotic case with table that uses subject title+namespace in container object (should never happen in SMW core)
563
						$from .= " INNER JOIN " . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " AS ids$tableIndex ON ids$tableIndex.smw_id=$joinfield" .
564
						         " INNER JOIN " . $db->tableName( $subproptable->getName() ) . " AS t$tableIndex ON " .
565
						         "t$tableIndex.s_title=ids$tableIndex.smw_title AND t$tableIndex.s_namespace=ids$tableIndex.smw_namespace";
566
					}
567
568
					if ( !$subproptable->isFixedPropertyTable() ) { // the ID we get should be !=0, so no point in filtering the converse
569 177
						$where .= ( $where ? ' AND ' : '' ) . "t$tableIndex.p_id=" . $db->addQuotes( $this->store->smwIds->getSMWPropertyID( $subproperty ) );
570 176
					}
571 176
572 176
					$this->prepareValueQuery( $from, $where, $subproptable, $subvalue, $tableIndex );
573
				}
574
			}
575 177
		} elseif ( !is_null( $value ) ) { // add conditions for given value
576
			$diHandler = $this->store->getDataItemHandlerForDIType( $value->getDIType() );
577
			foreach ( $diHandler->getWhereConds( $value ) as $fieldname => $value ) {
578
				$where .= ( $where ? ' AND ' : '' ) . "t$tableIndex.$fieldname=" . $db->addQuotes( $value );
579
			}
580
		}
581
	}
582
583
	/**
584
	 * @see SMWStore::getAllPropertySubjects
585 169
	 *
586 169
	 * @param SMWDIProperty $property
587
	 * @param SMWRequestOptions $requestOptions
588 169
	 *
589
	 * @return array of DIWikiPage
590
	 */
591
	public function getAllPropertySubjects( SMWDIProperty $property, SMWRequestOptions $requestOptions = null ) {
592
		$result = $this->getPropertySubjects( $property, null, $requestOptions );
593
594
		return $result;
595
	}
596
597
	/**
598
	 * @see Store::getProperties
599 1
	 *
600 1
	 * @param DIWikiPage $subject
601 1
	 * @param SMWRequestOptions|null $requestOptions
602 1
	 *
603 1
	 * @return SMWDataItem[]
604 1
	 */
605
	public function getProperties( DIWikiPage $subject, SMWRequestOptions $requestOptions = null ) {
606
		$sid = $this->store->smwIds->getSMWPageID(
607 1
			$subject->getDBkey(),
608 1
			$subject->getNamespace(),
609
			$subject->getInterwiki(),
610
			$subject->getSubobjectName()
611
		);
612
613
		if ( $sid == 0 ) { // no id, no page, no properties
614
			return array();
615
		}
616
617
		$db = $this->store->getConnection();
618
		$result = array();
619
620
		// potentially need to get more results, since options apply to union
621
		if ( $requestOptions !== null ) {
622
			$suboptions = clone $requestOptions;
623
			$suboptions->limit = $requestOptions->limit + $requestOptions->offset;
624
			$suboptions->offset = 0;
625
		} else {
626
			$suboptions = null;
627
		}
628
629
		foreach ( $this->store->getPropertyTables() as $propertyTable ) {
0 ignored issues
show
Bug introduced by
The expression $this->store->getPropertyTables() of type null|array<integer,objec...Store\TableDefinition>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
630
			if ( $propertyTable->usesIdSubject() ) {
631
				$where = 's_id=' . $db->addQuotes( $sid );
632
			} elseif ( $subject->getInterwiki() === '' ) {
633
				$where = 's_title=' . $db->addQuotes( $subject->getDBkey() ) . ' AND s_namespace=' . $db->addQuotes( $subject->getNamespace() );
634
			} else { // subjects with non-emtpy interwiki cannot have properties
635
				continue;
636
			}
637
638
			if ( $propertyTable->isFixedPropertyTable() ) {
639
				// just check if subject occurs in table
640
				$res = $db->select(
641
					$propertyTable->getName(),
642
					'*',
643
					$where,
644
					__METHOD__,
645
					array( 'LIMIT' => 1 )
646
				);
647
648
				if ( $db->numRows( $res ) > 0 ) {
649
					$result[] = new SMW\DIProperty( $propertyTable->getFixedProperty() );
650
				}
651
652
653
			} else {
654
				// select all properties
655
				$from = $db->tableName( $propertyTable->getName() );
656
657
				$from .= " INNER JOIN " . $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " ON smw_id=p_id";
658
				$res = $db->select( $from, 'DISTINCT smw_title,smw_sortkey',
659
					// (select sortkey since it might be used in ordering (needed by Postgres))
660
					$where . $this->store->getSQLConditions( $suboptions, 'smw_sortkey', 'smw_sortkey' ),
661
					__METHOD__, $this->store->getSQLOptions( $suboptions, 'smw_sortkey' ) );
662
				foreach ( $res as $row ) {
663
					$result[] = new SMW\DIProperty( $row->smw_title );
664
				}
665
			}
666
667
			$db->freeResult( $res );
668
		}
669
670
		// apply options to overall result
671
		$result = $this->store->applyRequestOptions( $result, $requestOptions );
672
673
674
		return $result;
675
	}
676
677
	/**
678
	 * Implementation of SMWStore::getInProperties(). This function is meant to
679
	 * be used for finding properties that link to wiki pages.
680
	 *
681
	 * @since 1.8
682
	 * @see SMWStore::getInProperties
683 19
	 *
684
	 * @param SMWDataItem $value
685 19
	 * @param SMWRequestOptions|null $requestOptions
686 19
	 *
687
	 * @return array of SMWWikiPageValue
688
	 */
689 19
	public function getInProperties( SMWDataItem $value, SMWRequestOptions $requestOptions = null ) {
690 6
691 6
		$db = $this->store->getConnection();
692 6
		$result = array();
693
694 13
		// Potentially need to get more results, since options apply to union.
695
		if ( $requestOptions !== null ) {
696
			$subOptions = clone $requestOptions;
697 19
			$subOptions->limit = $requestOptions->limit + $requestOptions->offset;
698
			$subOptions->offset = 0;
699 19
		} else {
700 19
			$subOptions = null;
701 19
		}
702
703
		$diType = $value->getDIType();
704 19
705 19
		foreach ( $this->store->getPropertyTables() as $proptable ) {
0 ignored issues
show
Bug introduced by
The expression $this->store->getPropertyTables() of type null|array<integer,objec...Store\TableDefinition>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
706 19
			if ( $diType != $proptable->getDiType() ) {
707 19
				continue;
708
			}
709 19
710
			$where = $from = '';
0 ignored issues
show
Unused Code introduced by
$from is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
711 19
			if ( !$proptable->isFixedPropertyTable() ) { // join ID table to get property titles
712
				$from = $db->tableName( SMWSql3SmwIds::TABLE_NAME ) . " INNER JOIN " . $db->tableName( $proptable->getName() ) . " AS t1 ON t1.p_id=smw_id";
713 19
				$this->prepareValueQuery( $from, $where, $proptable, $value, 1 );
714 19
715
				$where .= " AND smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWIW_OUTDATED ) . " AND smw_iw!=" . $db->addQuotes( SMW_SQL3_SMWDELETEIW );
716 19
717
				$res = $db->select( $from, 'DISTINCT smw_title,smw_sortkey,smw_iw',
718 4
						// select sortkey since it might be used in ordering (needed by Postgres)
719 19
						$where . $this->store->getSQLConditions( $subOptions, 'smw_sortkey', 'smw_sortkey', $where !== '' ),
720
						__METHOD__, $this->store->getSQLOptions( $subOptions, 'smw_sortkey' ) );
721
722
				foreach ( $res as $row ) {
723
					try {
724 19
						$result[] = new SMW\DIProperty( $row->smw_title );
725 19
					} catch (SMWDataItemException $e) {
726 19
						// has been observed to happen (empty property title); cause unclear; ignore this data
727
					}
728 19
				}
729 10
			} else {
730
				$from = $db->tableName( $proptable->getName() ) . " AS t1";
731
				$this->prepareValueQuery( $from, $where, $proptable, $value, 1 );
732 19
				$res = $db->select( $from, '*', $where, __METHOD__, array( 'LIMIT' => 1 ) );
733
734
				if ( $db->numRows( $res ) > 0 ) {
735 19
					$result[] = new SMW\DIProperty( $proptable->getFixedProperty() );
736
				}
737 19
			}
738
			$db->freeResult( $res );
739
		}
740
741
		$result = $this->store->applyRequestOptions( $result, $requestOptions ); // apply options to overall result
742
743
		return $result;
744
	}
745
746
}
747