ChangeNotificationFilter::hasChangeToNotifyAbout()   B
last analyzed

Complexity

Conditions 6
Paths 6

Size

Total Lines 39

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 6

Importance

Changes 0
Metric Value
dl 0
loc 39
ccs 22
cts 22
cp 1
rs 8.6737
c 0
b 0
f 0
cc 6
nc 6
nop 1
crap 6
1
<?php
2
3
namespace SMW\Notifications\ChangeNotification;
4
5
use SMW\Store;
6
use SMW\ApplicationFactory;
7
use SMW\DataValueFactory;
8
use SMW\DIWikiPage;
9
use SMW\DIProperty;
10
use SMW\SQLStore\CompositePropertyTableDiffIterator;
11
use SMW\Notifications\PropertyRegistry;
12
use User;
13
use Hooks;
14
15
/**
16
 * @license GNU GPL v2+
17
 * @since 1.0
18
 *
19
 * @author mwjames
20
 */
21
class ChangeNotificationFilter {
22
23
	const VALUE_CHANGE = 'smw-value-change';
24
	const SPECIFICATION_CHANGE = 'smw-specification-change';
25
26
	/**
27
	 * @var DIWikiPage
28
	 */
29
	private $subject;
30
31
	/**
32
	 * @var Store
33
	 */
34
	private $store;
35
36
	/**
37
	 * @var DataValueFactory
38
	 */
39
	private $dataValueFactory;
40
41
	/**
42
	 * @var array
43
	 */
44
	private $detectedProperties = array();
45
46
	/**
47
	 * In case detection matrix was stored as subobject on a property then match
48
	 * its pairs of property => subobjectKey so that later the Locator can find
49
	 * out which subobject contains the group that needs to be notified.
50
	 *
51
	 * @var array
52
	 */
53
	private $subSemanticDataMatch = array();
54
55
	/**
56
	 * @var string|null
57
	 */
58
	private $type = null;
59
60
	/**
61
	 * @var User|null
62
	 */
63
	private $agent = null;
64
65
	/**
66
	 * @var boolean
67
	 */
68
	private $canNotify = false;
69
70
	/**
71
	 * @var array
72
	 */
73
	private $propertyExemptionList = array();
74
75
	/**
76
	 * @var boolean
77
	 */
78
	private $isCommandLineMode = false;
79
80
	/**
81
	 * @since 1.0
82
	 *
83
	 * @param DIWikiPage $subject
84
	 * @param Store $store
85
	 */
86 8
	public function __construct( DIWikiPage $subject, Store $store ) {
87 8
		$this->subject = $subject;
88 8
		$this->store = $store;
89 8
		$this->dataValueFactory = DataValueFactory::getInstance();
90 8
	}
91
92
	/**
93
	 * @since 1.0
94
	 *
95
	 * @param User $agent
96
	 */
97 4
	public function setAgent( User $agent ) {
98 4
		$this->agent = $agent;
99 4
	}
100
101
	/**
102
	 * @since 1.0
103
	 *
104
	 * @param array $propertyExemptionList
105
	 */
106 1
	public function setPropertyExemptionList( array $propertyExemptionList ) {
107 1
		$this->propertyExemptionList = array_flip(
108 1
			str_replace( ' ', '_', $propertyExemptionList )
109
		);
110 1
	}
111
112
	/**
113
	 * @since 1.0
114
	 *
115
	 * @param boolean $isCommandLineMode
116
	 */
117
	public function isCommandLineMode( $isCommandLineMode ) {
118
		$this->isCommandLineMode = $isCommandLineMode;
119
	}
120
121
	/**
122
	 * @since 1.0
123
	 *
124
	 * @param CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator
125
	 *
126
	 * @return array
127
	 */
128
	public function findChangeEvent( CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator ) {
129
130
		// Avoid notification when run from the commandLine
131
		if ( $this->isCommandLineMode ) {
132
			return array();
133
		}
134
135
		return $this->getEventRecord( $this->hasChangeToNotifyAbout( $compositePropertyTableDiffIterator ) );
136
	}
137
138
	/**
139
	 * @see EchoEvent::create
140
	 *
141
	 * @since 1.0
142
	 *
143
	 * @param boolean|null $hasChangeToNotifyAbout
144
	 *
145
	 * @return array
146
	 */
147 2
	public function getEventRecord( $hasChangeToNotifyAbout = false ) {
148
149 2
		if ( $this->subject === null || $this->type === null || !$hasChangeToNotifyAbout ) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasChangeToNotifyAbout of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
150 1
			wfDebugLog( 'smw', 'EchoEvent was not triggered' );
151 1
			return array();
152
		}
153
154
		return array(
155 1
			'agent' => $this->agent,
156
			'extra' => array(
157
				'notifyAgent' => false,
158 1
				'revid'       => $this->subject->getTitle()->getLatestRevID(),
159 1
				'properties'  => $this->detectedProperties,
160 1
				'subSemanticDataMatch' => $this->subSemanticDataMatch,
161 1
				'subject'     => $this->subject
162
			),
163 1
			'title' => $this->subject->getTitle(),
164 1
			'type'  => $this->type
165
		);
166
	}
167
168
	/**
169
	 * @since 1.0
170
	 *
171
	 * @param CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator
172
	 *
173
	 * @return boolean|null
174
	 */
175 6
	public function hasChangeToNotifyAbout( CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator ) {
176
177 6
		$start = microtime( true );
178 6
		$this->type = self::VALUE_CHANGE;
179
180 6
		$property = new DIProperty(
181 6
			PropertyRegistry::NOTIFICATIONS_ON
182
		);
183
184
		if (
185 6
			$this->subject->getNamespace() === SMW_NS_PROPERTY ||
186 5
			$this->subject->getNamespace() === NS_CATEGORY ||
187 6
			$this->subject->getNamespace() === SMW_NS_CONCEPT ) {
188 1
			$this->type = self::SPECIFICATION_CHANGE;
189
		}
190
191 6
		foreach ( $compositePropertyTableDiffIterator->getTableChangeOps() as $tableChangeOp ) {
192
193 6
			if ( isset( $this->propertyExemptionList[$this->getFixedPropertyValueBy( $tableChangeOp, 'key' )] ) ) {
194 1
				continue;
195
			}
196
197 5
			$this->doFilterOnFieldChangeOps(
198 5
				$property,
199 5
				$tableChangeOp,
200 5
				$tableChangeOp->getFieldChangeOps( 'insert' )
201
			);
202
203 5
			$this->doFilterOnFieldChangeOps(
204 5
				$property,
205 5
				$tableChangeOp,
206 5
				$tableChangeOp->getFieldChangeOps( 'delete' )
207
			);
208
		}
209
210 6
		wfDebugLog( 'smw', __METHOD__ . ' ' .  $this->subject->getHash() . ' in procTime (sec): ' . round( ( microtime( true ) - $start ), 7 ) );
211
212 6
		return $this->canNotify;
213
	}
214
215
	// 2.4 compat
216 6
	private function getFixedPropertyValueBy( $tableChangeOp, $key ) {
217 6
		return method_exists( $tableChangeOp, 'getFixedPropertyValueFor' ) ? $tableChangeOp->getFixedPropertyValueFor( $key ) : $tableChangeOp->getFixedPropertyValueBy( $key );
218
	}
219
220
	// 2.4 compat
221 5
	private function getDataItemById( $id ) {
222 5
		return method_exists( $this->store->getObjectIds(), 'getDataItemForId' ) ?  $this->store->getObjectIds()->getDataItemForId( $id ) : $this->store->getObjectIds()->getDataItemById( $id );
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...
223
	}
224
225 5
	private function doFilterOnFieldChangeOps( $property, $tableChangeOp, $fieldChangeOps ) {
226
227 5
		foreach ( $fieldChangeOps as $fieldChangeOp ) {
228
229
			// _INST is special since the p_id doesn't play a role
230
			// in determining the category page involved
231 5
			if ( $tableChangeOp->isFixedPropertyOp() ) {
232 3
				if ( $this->getFixedPropertyValueBy( $tableChangeOp, 'key' ) === '_INST' ) {
233
					$fieldChangeOp->set( 'p_id', $fieldChangeOp->get( 'o_id' ) );
234
				} else {
235 3
					$fieldChangeOp->set( 'p_id', $this->getFixedPropertyValueBy( $tableChangeOp, 'p_id' ) );
236
				}
237
			}
238
239
			// Get DI representation to build a DataValues that allows
240
			// to match/compare values to its serialization form
241 5
			$dataItem = $this->getDataItemById(
242 5
				$fieldChangeOp->get( 'p_id' )
243
			);
244
245 5
			if ( $dataItem === null || $dataItem->getDBKey() === '' || isset( $this->propertyExemptionList[$dataItem->getDBKey()] ) ) {
246
				continue;
247
			}
248
249
			// Shortcut! we know changes occurred on a property itself
250 5
			if ( $this->type === self::SPECIFICATION_CHANGE ) {
251 1
				$this->detectedProperties[$dataItem->getHash()] = $dataItem;
252 1
				$this->canNotify = true;
253 1
				continue;
254
			}
255
256 4
			$this->doCompareNotificationsOnValuesWithOps(
257 4
				$property,
258 4
				$dataItem,
259 4
				$fieldChangeOp
260
			);
261
		}
262 5
	}
263
264 4
	private function doCompareNotificationsOnValuesWithOps( $property, $dataItem, $fieldChangeOp ) {
265
266 4
		$propertySpecificationLookup = ApplicationFactory::getInstance()->getPropertySpecificationLookup();
267
268
		// Don't mix !!
269
		// Either use the plain annotation style via [[Notifications on:: ...]] OR
270
		// in case one wants to have a detection matrix use:
271
		//
272
		// {{#subobject:
273
		//  |Notifications on=...
274
		//  |Notifications to group=...
275
		// }}
276 4
		if ( ( $pv = $propertySpecificationLookup->getSpecification( $dataItem, $property ) ) !== array() ) {
277 3
			return $this->doCompareOnPropertyValues( $dataItem, $pv, $fieldChangeOp );
278
		}
279
280
		// Get the whole property definitions and compare on subobjects that
281
		// contain `Notifications on:: ...` declarations
282 1
		$semanticData = $this->store->getSemanticData(
283 1
			$dataItem
284
		);
285
286
		// If matched then remember the subobjectName to later during the UserLocator
287
		// process to find out which groups on a particular SOBJ are to be
288
		// addressed
289 1
		foreach ( $semanticData->getSubSemanticData() as $subSemanticData ) {
290 1
			if ( $subSemanticData->hasProperty( $property ) ) {
291 1
				$this->doCompareOnPropertyValues(
292 1
					$dataItem,
293 1
					$subSemanticData->getPropertyValues( $property ),
294 1
					$fieldChangeOp,
295 1
					$subSemanticData->getSubject()->getSubobjectName()
296
				);
297
			}
298
		}
299 1
	}
300
301 4
	private function doCompareOnPropertyValues( $dataItem, $propertyValues, $fieldChangeOp, $subobjectName = null ) {
302 4
		foreach ( $propertyValues as $val ) {
303 4
			$this->doCompareFields( $val->getString(), $fieldChangeOp, $dataItem, $subobjectName );
304
		}
305 4
	}
306
307 4
	private function doCompareFields( $value, $fieldChangeOp, $dataItem, $subobjectName ) {
308
309 4
		$hash = $dataItem->getHash();
310
311
		// Any value
312 4
		if ( $value === '+' ) {
313 1
			$this->detectedProperties[$hash] = $dataItem;
314 1
			$this->subSemanticDataMatch[$hash][] = $subobjectName;
315 1
			$this->canNotify = true;
316 3
		} elseif ( $fieldChangeOp->has( 'o_serialized' ) || $fieldChangeOp->has( 'o_blob' ) ) {
317
318
			// Literal object entities
319 1
			if ( $fieldChangeOp->has( 'o_serialized' ) ) {
320
				$string = $fieldChangeOp->get( 'o_serialized' );
321 1
			} elseif ( $fieldChangeOp->get( 'o_blob' ) ) {
322 1
				$string = $fieldChangeOp->get( 'o_blob' );
323
			} else {
324
				$string = $fieldChangeOp->get( 'o_hash' );
325
			}
326
327 1
			$dataValue = $this->dataValueFactory->newDataValueByProperty(
328 1
				DIProperty::newFromUserLabel( $dataItem->getDBKey() ),
329 1
				$value
330
			);
331
332
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
55% 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...
333
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $dataValue->getDataItem() ) ) === false ) {
334
				return;
335
			}
336
*/
337 1
			if ( $string === $dataValue->getDataItem()->getSerialization() ) {
338 1
				$this->detectedProperties[$hash] = $dataItem;
339 1
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
340 1
				$this->canNotify = true;
341
			}
342 2
		} elseif ( $fieldChangeOp->has( 'o_id' ) ) {
343
344
			// Page object entities
345 2
			$oDataItem = $this->getDataItemById(
346 2
				$fieldChangeOp->get( 'o_id' )
347
			);
348
/*
0 ignored issues
show
Unused Code Comprehensibility introduced by
54% 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...
349
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $oDataItem ) ) === false ) {
350
				return;
351
			}
352
*/
353 2
			if ( $value === str_replace( '_', ' ', $oDataItem->getDBKey() ) ) {
354 2
				$this->detectedProperties[$hash] = $dataItem;
355 2
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
356 2
				$this->canNotify = true;
357
			}
358
		}
359 4
	}
360
361
}
362