Completed
Push — master ( eae6a8...e2f648 )
by mw
20:21
created

ChangeNotificationFilter   B

Complexity

Total Complexity 37

Size/Duplication

Total Lines 292
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Test Coverage

Coverage 96.03%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 37
c 1
b 0
f 0
lcom 1
cbo 8
dl 0
loc 292
ccs 121
cts 126
cp 0.9603
rs 8.6

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A setAgent() 0 3 1
A getEventRecord() 0 20 4
C hasChangeToNotifyAbout() 0 42 7
A getFixedPropertyValueBy() 0 3 2
C doFilterOnFieldChangeOps() 0 38 7
B doCompareNotificationsOnValuesWithOps() 0 36 4
A doCompareOnPropertyValues() 0 5 2
C doCompareFields() 0 53 9
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
	 * @since 1.0
72
	 *
73
	 * @param DIWikiPage $subject
74
	 * @param Store $store
75
	 */
76 8
	public function __construct( DIWikiPage $subject, Store $store ) {
77 8
		$this->subject = $subject;
78 8
		$this->store = $store;
79 8
		$this->dataValueFactory = DataValueFactory::getInstance();
80 8
	}
81
82
	/**
83
	 * @since 1.0
84
	 *
85
	 * @param User $agent
86
	 */
87 4
	public function setAgent( User $agent ) {
88 4
		$this->agent = $agent;
89 4
	}
90
91
	/**
92
	 * @see EchoEvent::create
93
	 *
94
	 * @since 1.0
95
	 *
96
	 * @param boolean|null $hasChangeToNotifyAbout
97
	 *
98
	 * @return array
99
	 */
100 2
	public function getEventRecord( $hasChangeToNotifyAbout = false ) {
101
102 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...
103 1
			wfDebugLog( 'smw', 'EchoEvent was not triggered' );
104 1
			return array();
105
		}
106
107
		return array(
108 1
			'agent' => $this->agent,
109
			'extra' => array(
110 1
				'notifyAgent' => false,
111 1
				'revid'       => $this->subject->getTitle()->getLatestRevID(),
112 1
				'properties'  => $this->detectedProperties,
113 1
				'subSemanticDataMatch' => $this->subSemanticDataMatch,
114 1
				'subject'     => $this->subject
115 1
			),
116 1
			'title' => $this->subject->getTitle(),
117 1
			'type'  => $this->type
118 1
		);
119
	}
120
121
	/**
122
	 * @since 1.0
123
	 *
124
	 * @param CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator
125
	 *
126
	 * @return boolean|null
127
	 */
128 6
	public function hasChangeToNotifyAbout( CompositePropertyTableDiffIterator $compositePropertyTableDiffIterator ) {
129
130 6
		$start = microtime( true );
131 6
		$this->type = self::VALUE_CHANGE;
132
133 6
		$property = new DIProperty(
134
			PropertyRegistry::NOTIFICATIONS_ON
135 6
		);
136
137
		if (
138 6
			$this->subject->getNamespace() === SMW_NS_PROPERTY ||
139 5
			$this->subject->getNamespace() === NS_CATEGORY ||
140 6
			$this->subject->getNamespace() === SMW_NS_CONCEPT ) {
141 1
			$this->type = self::SPECIFICATION_CHANGE;
142 1
		}
143
144 6
		foreach ( $compositePropertyTableDiffIterator->getTableChangeOps() as $tableChangeOp ) {
145
146
			// Skip the Modification date
147
			if (
148 6
				( $this->getFixedPropertyValueBy( $tableChangeOp, 'key' ) === '_MDAT' ) ||
149 6
				( $this->getFixedPropertyValueBy( $tableChangeOp, 'key' ) === '_REDI' ) ) {
150 1
				continue;
151
			}
152
153 5
			$this->doFilterOnFieldChangeOps(
154 5
				$property,
155 5
				$tableChangeOp,
156 5
				$tableChangeOp->getFieldChangeOps( 'insert' )
157 5
			);
158
159 5
			$this->doFilterOnFieldChangeOps(
160 5
				$property,
161 5
				$tableChangeOp,
162 5
				$tableChangeOp->getFieldChangeOps( 'delete' )
163 5
			);
164 6
		}
165
166 6
		wfDebugLog( 'smw', __METHOD__ . ' ' .  $this->subject->getHash() . ' in procTime (sec): ' . round( ( microtime( true ) - $start ), 7 ) );
167
168 6
		return $this->canNotify;
169
	}
170
171 5
	// 2.4 compat
172
	private function getFixedPropertyValueBy( $tableChangeOp, $key ) {
173 5
		return method_exists( $tableChangeOp, 'getFixedPropertyValueFor' ) ? $tableChangeOp->getFixedPropertyValueFor( $key ) : $tableChangeOp->getFixedPropertyValueBy( $key );
174
	}
175
176
	private function doFilterOnFieldChangeOps( $property, $tableChangeOp, $fieldChangeOps ) {
177 5
178 3
		foreach ( $fieldChangeOps as $fieldChangeOp ) {
179
180
			// _INST is special since the p_id doesn't play a role
181 3
			// in determining the category page involved
182
			if ( $tableChangeOp->isFixedPropertyOp() ) {
183 3
				if ( $this->getFixedPropertyValueBy( $tableChangeOp, 'key' ) === '_INST' ) {
184
					$fieldChangeOp->set( 'p_id', $fieldChangeOp->get( 'o_id' ) );
185
				} else {
186
					$fieldChangeOp->set( 'p_id', $this->getFixedPropertyValueBy( $tableChangeOp, 'p_id' ) );
187 5
				}
188 5
			}
189 5
190
			// Get DI representation to build a DataValues that allows
191 5
			// to match/compare values to its serialization form
192
			$dataItem = $this->store->getObjectIds()->getDataItemById(
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...
193
				$fieldChangeOp->get( 'p_id' )
194
			);
195
196 5
			if ( $dataItem === null || $dataItem->getDBKey() === '' ) {
197 1
				continue;
198 1
			}
199 1
200
			// Shortcut! we know changes occurred on a property itself
201
			if ( $this->type === self::SPECIFICATION_CHANGE ) {
202 4
				$this->detectedProperties[$dataItem->getHash()] = $dataItem;
203 4
				$this->canNotify = true;
204 4
				continue;
205
			}
206 4
207 5
			$this->doCompareNotificationsOnValuesWithOps(
208 5
				$property,
209
				$dataItem,
210 4
				$fieldChangeOp
211
			);
212 4
		}
213
	}
214
215
	private function doCompareNotificationsOnValuesWithOps( $property, $dataItem, $fieldChangeOp ) {
216
217
		$cachedPropertyValuesPrefetcher = ApplicationFactory::getInstance()->getCachedPropertyValuesPrefetcher();
218
219
		// Don't mix !!
220
		// Either use the plain annotation style via [[Notifications on:: ...]] OR
221
		// in case one wants to have a detection matrix use:
222 4
		//
223 3
		// {{#subobject:
224
		//  |Notifications on=...
225
		//  |Notifications to group=...
226
		// }}
227
		if ( ( $pv = $cachedPropertyValuesPrefetcher->getPropertyValues( $dataItem, $property ) ) !== array() ) {
228 1
			return $this->doCompareOnPropertyValues( $dataItem, $pv, $fieldChangeOp );
229
		}
230 1
231
		// Get the whole property definitions and compare on subobjects that
232
		// contain `Notifications on:: ...` declarations
233
		$semanticData = $this->store->getSemanticData(
234
			$dataItem
235 1
		);
236 1
237 1
		// If matched then remember the subobjectName to later during the UserLocator
238 1
		// process to find out which groups on a particular SOBJ are to be
239 1
		// addressed
240 1
		foreach ( $semanticData->getSubSemanticData() as $subSemanticData ) {
241 1
			if ( $subSemanticData->hasProperty( $property ) ) {
242 1
				$this->doCompareOnPropertyValues(
243 1
					$dataItem,
244 1
					$subSemanticData->getPropertyValues( $property ),
245 1
					$fieldChangeOp,
246
					$subSemanticData->getSubject()->getSubobjectName()
247 4
				);
248 4
			}
249 4
		}
250 4
	}
251 4
252
	private function doCompareOnPropertyValues( $dataItem, $propertyValues, $fieldChangeOp, $subobjectName = null ) {
253 4
		foreach ( $propertyValues as $val ) {
254
			$this->doCompareFields( $val->getString(), $fieldChangeOp, $dataItem, $subobjectName );
255 4
		}
256
	}
257
258 4
	private function doCompareFields( $value, $fieldChangeOp, $dataItem, $subobjectName ) {
259 1
260 1
		$hash = $dataItem->getHash();
261 1
262 4
		// Any value
263
		if ( $value === '+' ) {
264
			$this->detectedProperties[$hash] = $dataItem;
265 1
			$this->subSemanticDataMatch[$hash][] = $subobjectName;
266
			$this->canNotify = true;
267 1
		} elseif ( $fieldChangeOp->has( 'o_serialized' ) || $fieldChangeOp->has( 'o_blob' ) ) {
268 1
269 1
			// Literal object entities
270
			if ( $fieldChangeOp->has( 'o_serialized' ) ) {
271
				$string = $fieldChangeOp->get( 'o_serialized' );
272
			} elseif ( $fieldChangeOp->get( 'o_blob' ) ) {
273 1
				$string = $fieldChangeOp->get( 'o_blob' );
274 1
			} else {
275
				$string = $fieldChangeOp->get( 'o_hash' );
276 1
			}
277
278
			$dataValue = $this->dataValueFactory->newDataValueByProperty(
279
				DIProperty::newFromUserLabel( $dataItem->getDBKey() ),
280
				$value
281
			);
282
283 1
/*
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...
284 1
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $dataValue->getDataItem() ) ) === false ) {
285 1
				return;
286 1
			}
287 1
*/
288 3
			if ( $string === $dataValue->getDataItem()->getSerialization() ) {
289
				$this->detectedProperties[$hash] = $dataItem;
290
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
291 2
				$this->canNotify = true;
292 2
			}
293 2
		} elseif ( $fieldChangeOp->has( 'o_id' ) ) {
294
295
			// Page object entities
296
			$oDataItem = $this->store->getObjectIds()->getDataItemById(
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...
297
				$fieldChangeOp->get( 'o_id' )
298
			);
299 2
/*
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...
300 2
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $oDataItem ) ) === false ) {
301 2
				return;
302 2
			}
303 2
*/
304 2
			if ( $value === str_replace( '_', ' ', $oDataItem->getDBKey() ) ) {
305 4
				$this->detectedProperties[$hash] = $dataItem;
306
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
307
				$this->canNotify = true;
308
			}
309
		}
310
	}
311
312
}
313