Completed
Push — master ( f40723...0452ba )
by mw
90:24 queued 55:06
created

QueryDependencyLinksStore::setEnabled()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SMW\SQLStore\QueryDependency;
4
5
use SMW\ApplicationFactory;
6
use SMW\DIProperty;
7
use SMW\DIWikiPage;
8
use SMW\EventHandler;
9
use SMW\SQLStore\CompositePropertyTableDiffIterator;
10
use SMW\Store;
11
use SMW\RequestOptions;
12
use SMWSQLStore3;
13
14
/**
15
 * @license GNU GPL v2+
16
 * @since 2.3
17
 *
18
 * @author mwjames
19
 */
20
class QueryDependencyLinksStore {
21
22
	/**
23
	 * @var Store
24
	 */
25
	private $store = null;
26
27
	/**
28
	 * @var DependencyLinksTableUpdater
29
	 */
30
	private $dependencyLinksTableUpdater = null;
31
32
	/**
33
	 * @var Database
34
	 */
35
	private $connection = null;
36
37
	/**
38
	 * @var boolean
39
	 */
40
	private $isEnabled = true;
41
42
	/**
43
	 * Time factor to be used to determine whether an update should actually occur
44
	 * or not. The comparison is made against the page_touched timestamp (updated
45
	 * by the ParserCachePurgeJob) to a previous update to avoid unnecessary DB
46
	 * transactions if it takes place within the computed time frame.
47
	 *
48
	 * @var integer
49
	 */
50
	private $skewFactorForDepedencyUpdateInSeconds = 10;
51
52
	/**
53
	 * @since 2.3
54
	 *
55
	 * @param DependencyLinksTableUpdater $dependencyLinksTableUpdater
56 191
	 */
57 191
	public function __construct( DependencyLinksTableUpdater $dependencyLinksTableUpdater ) {
58 191
		$this->dependencyLinksTableUpdater = $dependencyLinksTableUpdater;
59 191
		$this->store = $this->dependencyLinksTableUpdater->getStore();
60 191
		$this->connection = $this->store->getConnection( 'mw.db' );
61
	}
62
63
	/**
64
	 * @since 2.3
65
	 *
66
	 * @return boolean
67 190
	 */
68 190
	public function isEnabled() {
69
		return $this->isEnabled;
70
	}
71
72
	/**
73
	 * @since 2.3
74
	 *
75
	 * @param boolean $isEnabled
76 184
	 */
77 184
	public function setEnabled( $isEnabled ) {
78 184
		$this->isEnabled = (bool)$isEnabled;
79
	}
80
81
	/**
82
	 * This method is called from the `SMW::SQLStore::AfterDataUpdateComplete` hook and
83
	 * removes outdated query ID's from the table if the diff contains a `delete`
84
	 * entry for the _ask table.
85
	 *
86
	 * @since 2.3
87
	 *
88
	 * @param CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator
89 182
	 */
90
	public function pruneOutdatedTargetLinks( DIWikiPage $subject, CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator ) {
91 182
92 1
		if ( !$this->isEnabled() ) {
93
			return null;
94
		}
95 181
96 181
		$start = microtime( true );
97
		$tableName = $this->store->getPropertyTableInfoFetcher()->findTableIdForProperty(
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 getPropertyTableInfoFetcher() does only exist in the following sub-classes of SMW\Store: SMWSQLStore3. 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...
98
			new DIProperty( '_ASK' )
99 181
		);
100
101
		$tableChangeOps = $compositePropertyTableDiffIterator->getTableChangeOps( $tableName );
102 181
103
		// Remove any dependency for queries that are no longer used
104 72
		foreach ( $tableChangeOps as $tableChangeOp ) {
105 72
106
			if ( !$tableChangeOp->hasChangeOp( 'delete' ) ) {
107
				continue;
108 72
			}
109
110 72
			$deleteIdList = array();
111 72
112
			foreach ( $tableChangeOp->getFieldChangeOps( 'delete' ) as $fieldChangeOp ) {
113
				$deleteIdList[] = $fieldChangeOp->get( 'o_id' );
114 72
			}
115
116
			$this->dependencyLinksTableUpdater->deleteDependenciesFromList( $deleteIdList );
117
		}
118
119 181
		wfDebugLog( 'smw', __METHOD__ . ' finished on ' . $subject->getHash() . ' with procTime (sec): ' . round( ( microtime( true ) - $start ) ,7 ) );
120 181
121
		return true;
122
	}
123 181
124 181
	/**
125
	 * Build the ParserCachePurgeJob parameters on filtered entities to minimize
126
	 * necessary update work.
127 181
	 *
128
	 * @since 2.3
129
	 *
130
	 * @param EntityIdListRelevanceDetectionFilter $entityIdListRelevanceDetectionFilter
131
	 *
132
	 * @return array
133
	 */
134
	public function buildParserCachePurgeJobParametersFrom( EntityIdListRelevanceDetectionFilter $entityIdListRelevanceDetectionFilter ) {
135
136
		if ( !$this->isEnabled() ) {
137
			return array();
138
		}
139
140 182
		return array(
141
			'idlist' => $entityIdListRelevanceDetectionFilter->getFilteredIdList()
142 182
		);
143 1
	}
144
145
	/**
146
	 * @since 2.5
147 181
	 *
148
	 * @param DIWikiPage $subject
149
	 * @param RequestOptions|null $requestOptions
150
	 *
151
	 * @return array
152
	 */
153
	public function findEmbeddedQueryIdListBySubject( DIWikiPage $subject, RequestOptions $requestOptions = null ) {
154
155
		$embeddedQueryIdList = array();
156
157
		$dataItems = $this->store->getPropertyValues(
158
			$subject,
159
			new DIProperty( '_ASK' ),
160
			$requestOptions
161
		);
162
163
		foreach ( $dataItems as $dataItem ) {
164
			$embeddedQueryIdList[$dataItem->getHash()] = $this->dependencyLinksTableUpdater->getId( $dataItem );
165
		}
166
167
		return $embeddedQueryIdList;
168
	}
169
170
	/**
171
	 * Finds a partial list (given limit and offset) of registered subjects that
172
	 * that represent a dependency on something like a subject in a query list,
173
	 * a property, or a printrequest.
174
	 *
175 1
	 * `s_id` contains the subject id that links to the query that fulfills one
176
	 * of the conditions cited above.
177 1
	 *
178
	 * Prefetched Ids are turned into a hash list that can later be split into
179
	 * chunks to work either in online or batch mode without creating a huge memory
180
	 * foothold.
181
	 *
182 1
	 * @note Select a list is crucial for performance as any selectRow would /
183 1
	 * single Id select would strain the system on large list connected to a
184 1
	 * query
185 1
	 *
186
	 * @since 2.3
187
	 *
188
	 * @param array $idlist
189 1
	 * @param RequestOptions $requestOptions
190 1
	 *
191 1
	 * @return array
192
	 */
193 1
	public function findEmbeddedQueryTargetLinksHashListFor( array $idlist, RequestOptions $requestOptions ) {
194
195 1
		if ( $idlist === array() || !$this->isEnabled() ) {
196
			return array();
197
		}
198
199 1
		$options = array(
200
			'LIMIT'     => $requestOptions->getLimit(),
201 1
			'OFFSET'    => $requestOptions->getOffset(),
202 1
			'GROUP BY'  => 's_id',
203
			'ORDER BY'  => 's_id',
204
			'DISTINCT'  => true
205 1
		);
206
207
		$conditions = array(
208
			'o_id' => $idlist
209 1
		);
210
211
		foreach ( $requestOptions->getExtraConditions() as $extraCondition ) {
212
			$conditions += $extraCondition;
213
		}
214
215
		$rows = $this->connection->select(
216
			SMWSQLStore3::QUERY_LINKS_TABLE,
217
			array( 's_id' ),
218
			$conditions,
219
			__METHOD__,
220
			$options
221
		);
222
223 123
		$targetLinksIdList = array();
224
225 123
		foreach ( $rows as $row ) {
226 59
			$targetLinksIdList[] = $row->s_id;
227
		}
228
229 70
		if ( $targetLinksIdList === array() ) {
230 70
			return array();
231
		}
232 70
233
		return $this->store->getObjectIds()->getDataItemPoolHashListFor(
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 getObjectIds() does only exist in the following sub-classes of SMW\Store: SMWSQLStore3, SMWSparqlStore, SMW\SPARQLStore\SPARQLStore. 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...
234
			$targetLinksIdList
235
		);
236
	}
237 70
238 1
	/**
239
	 * This method is called from the `SMW::Store::AfterQueryResultLookupComplete` hook
240
	 * to resolve and update dependencies fetched from an embedded query and its
241 69
	 * QueryResult object.
242
	 *
243 69
	 * @since 2.3
244 69
	 *
245
	 * @param QueryResultDependencyListResolver $queryResultDependencyListResolver
246
	 */
247 69
	public function doUpdateDependenciesBy( QueryResultDependencyListResolver $queryResultDependencyListResolver ) {
0 ignored issues
show
Coding Style introduced by
doUpdateDependenciesBy 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...
248
249
		if ( !$this->isEnabled() || $queryResultDependencyListResolver->getSubject() === null ) {
250
			return null;
251
		}
252
253 69
		$start = microtime( true );
254
255 69
		$subject = $queryResultDependencyListResolver->getSubject();
256
		$hash = $queryResultDependencyListResolver->getQueryId();
257
258 69
		$sid = $this->dependencyLinksTableUpdater->getId(
259 69
			$subject,
260
			$hash
261 69
		);
262
263
		if ( $this->canSuppressUpdateOnSkewFactorFor( $sid, $subject ) ) {
264
			return wfDebugLog( 'smw', __METHOD__ . " suppressed (skewed time) for SID " . $sid . "\n" );
265 69
		}
266 69
267
		$dependencyLinksTableUpdater = $this->dependencyLinksTableUpdater;
268 69
269 61
		$deferredCallableUpdate = ApplicationFactory::getInstance()->newDeferredCallableUpdate( function() use( $subject, $sid, $hash, $dependencyLinksTableUpdater, $queryResultDependencyListResolver ) {
270
271
			$dependencyList = $queryResultDependencyListResolver->getDependencyList();
272
273
			// Add extra dependencies which we only get "late" after the QueryResult
274
			// object as been resolved by the ResultPrinter, this is done to
275 66
			// avoid having to process the QueryResult recursively on its own
276 66
			// (which would carry a performance penalty)
277
			$dependencyListByLateRetrieval = $queryResultDependencyListResolver->getDependencyListByLateRetrieval();
278 66
279
			if ( $dependencyList === array() && $dependencyListByLateRetrieval === array() ) {
280
				return wfDebugLog( 'smw', 'No dependency list available ' . $hash );
281
			}
282
283 66
			// SID < 0 means the storage update/process has not been finalized
284
			// (new object hasn't been registered)
285 66
			if ( $sid < 1 || ( $sid = $dependencyLinksTableUpdater->getId( $subject, $hash ) ) < 1 ) {
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $sid, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
286
				$sid = $dependencyLinksTableUpdater->createId( $subject, $hash );
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $sid, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
287
			}
288 66
289 66
			$dependencyLinksTableUpdater->addToUpdateList(
290 66
				$sid,
291 66
				$dependencyList
292
			);
293
294
			$dependencyLinksTableUpdater->addToUpdateList(
295
				$sid,
296
				$dependencyListByLateRetrieval
297
			);
298
299 70
			$dependencyLinksTableUpdater->doUpdate();
300 70
		} );
301 70
302 70
		$deferredCallableUpdate->setOrigin( __METHOD__ );
303 70
304
		// https://www.mediawiki.org/wiki/Manual:$wgCommandLineMode
305 70
		// Indicates whether MW is running in command-line mode.
306
		$deferredCallableUpdate->markAsPending( $GLOBALS['wgCommandLineMode'] );
307
		$deferredCallableUpdate->enabledDeferredUpdate( true );
308
		$deferredCallableUpdate->pushUpdate();
309 70
310
		wfDebugLog( 'smw', __METHOD__ . ' procTime (sec): ' . round( ( microtime( true ) - $start ), 7 ) );
311 70
	}
312 70
313
	private function canSuppressUpdateOnSkewFactorFor( $sid, $subject ) {
314 70
315 66
		static $suppressUpdateCache = array();
316
		$hash = $subject->getHash();
317
		if ( $sid < 1 ) {
318 62
			return false;
319 62
		}
320
321
		$row = $this->connection->selectRow(
322 62
			SMWSQLStore3::QUERY_LINKS_TABLE,
323 62
			array(
324 62
				's_id'
325
			),
326
			array( 's_id' => $sid ),
327 62
			__METHOD__
328 62
		);
329
330
		if ( !isset( $suppressUpdateCache[$hash] ) ) {
331
			$suppressUpdateCache[$hash] = $subject->getTitle()->getTouched() + $this->skewFactorForDepedencyUpdateInSeconds;
332
		}
333 62
334
		// Check whether the query has already been registered and only then
335
		// check for a possible divergent time
336
		return $row !== false && $suppressUpdateCache[$hash] > wfTimestamp( TS_MW );
337
	}
338
339
}
340