Completed
Push — master ( 1e5fc8...4f25e0 )
by mw
17:35
created

ChangeNotificationFilter::getDataItemById()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 3
ccs 1
cts 1
cp 1
rs 10
cc 2
eloc 2
nc 2
nop 1
crap 2
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
	// 2.4 compat
172 6
	private function getFixedPropertyValueBy( $tableChangeOp, $key ) {
173 6
		return method_exists( $tableChangeOp, 'getFixedPropertyValueFor' ) ? $tableChangeOp->getFixedPropertyValueFor( $key ) : $tableChangeOp->getFixedPropertyValueBy( $key );
174
	}
175
176 5
	// 2.4 compat
177
	private function getDataItemById( $id ) {
178 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...
179
	}
180
181
	private function doFilterOnFieldChangeOps( $property, $tableChangeOp, $fieldChangeOps ) {
182 5
183 3
		foreach ( $fieldChangeOps as $fieldChangeOp ) {
184
185
			// _INST is special since the p_id doesn't play a role
186 3
			// in determining the category page involved
187
			if ( $tableChangeOp->isFixedPropertyOp() ) {
188 3
				if ( $this->getFixedPropertyValueBy( $tableChangeOp, 'key' ) === '_INST' ) {
189
					$fieldChangeOp->set( 'p_id', $fieldChangeOp->get( 'o_id' ) );
190
				} else {
191
					$fieldChangeOp->set( 'p_id', $this->getFixedPropertyValueBy( $tableChangeOp, 'p_id' ) );
192 5
				}
193 5
			}
194 5
195
			// Get DI representation to build a DataValues that allows
196 5
			// to match/compare values to its serialization form
197
			$dataItem = $this->getDataItemById(
198
				$fieldChangeOp->get( 'p_id' )
199
			);
200
201 5
			if ( $dataItem === null || $dataItem->getDBKey() === '' ) {
202 1
				continue;
203 1
			}
204 1
205
			// Shortcut! we know changes occurred on a property itself
206
			if ( $this->type === self::SPECIFICATION_CHANGE ) {
207 4
				$this->detectedProperties[$dataItem->getHash()] = $dataItem;
208 4
				$this->canNotify = true;
209 4
				continue;
210
			}
211 4
212 5
			$this->doCompareNotificationsOnValuesWithOps(
213 5
				$property,
214
				$dataItem,
215 4
				$fieldChangeOp
216
			);
217 4
		}
218
	}
219
220
	private function doCompareNotificationsOnValuesWithOps( $property, $dataItem, $fieldChangeOp ) {
221
222
		$cachedPropertyValuesPrefetcher = ApplicationFactory::getInstance()->getCachedPropertyValuesPrefetcher();
223
224
		// Don't mix !!
225
		// Either use the plain annotation style via [[Notifications on:: ...]] OR
226
		// in case one wants to have a detection matrix use:
227 4
		//
228 3
		// {{#subobject:
229
		//  |Notifications on=...
230
		//  |Notifications to group=...
231
		// }}
232
		if ( ( $pv = $cachedPropertyValuesPrefetcher->getPropertyValues( $dataItem, $property ) ) !== array() ) {
233 1
			return $this->doCompareOnPropertyValues( $dataItem, $pv, $fieldChangeOp );
234
		}
235 1
236
		// Get the whole property definitions and compare on subobjects that
237
		// contain `Notifications on:: ...` declarations
238
		$semanticData = $this->store->getSemanticData(
239
			$dataItem
240 1
		);
241 1
242 1
		// If matched then remember the subobjectName to later during the UserLocator
243 1
		// process to find out which groups on a particular SOBJ are to be
244 1
		// addressed
245 1
		foreach ( $semanticData->getSubSemanticData() as $subSemanticData ) {
246 1
			if ( $subSemanticData->hasProperty( $property ) ) {
247 1
				$this->doCompareOnPropertyValues(
248 1
					$dataItem,
249 1
					$subSemanticData->getPropertyValues( $property ),
250 1
					$fieldChangeOp,
251
					$subSemanticData->getSubject()->getSubobjectName()
252 4
				);
253 4
			}
254 4
		}
255 4
	}
256 4
257
	private function doCompareOnPropertyValues( $dataItem, $propertyValues, $fieldChangeOp, $subobjectName = null ) {
258 4
		foreach ( $propertyValues as $val ) {
259
			$this->doCompareFields( $val->getString(), $fieldChangeOp, $dataItem, $subobjectName );
260 4
		}
261
	}
262
263 4
	private function doCompareFields( $value, $fieldChangeOp, $dataItem, $subobjectName ) {
264 1
265 1
		$hash = $dataItem->getHash();
266 1
267 4
		// Any value
268
		if ( $value === '+' ) {
269
			$this->detectedProperties[$hash] = $dataItem;
270 1
			$this->subSemanticDataMatch[$hash][] = $subobjectName;
271
			$this->canNotify = true;
272 1
		} elseif ( $fieldChangeOp->has( 'o_serialized' ) || $fieldChangeOp->has( 'o_blob' ) ) {
273 1
274 1
			// Literal object entities
275
			if ( $fieldChangeOp->has( 'o_serialized' ) ) {
276
				$string = $fieldChangeOp->get( 'o_serialized' );
277
			} elseif ( $fieldChangeOp->get( 'o_blob' ) ) {
278 1
				$string = $fieldChangeOp->get( 'o_blob' );
279 1
			} else {
280
				$string = $fieldChangeOp->get( 'o_hash' );
281 1
			}
282
283
			$dataValue = $this->dataValueFactory->newDataValueByProperty(
284
				DIProperty::newFromUserLabel( $dataItem->getDBKey() ),
285
				$value
286
			);
287
288 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...
289 1
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $dataValue->getDataItem() ) ) === false ) {
290 1
				return;
291 1
			}
292 1
*/
293 3
			if ( $string === $dataValue->getDataItem()->getSerialization() ) {
294
				$this->detectedProperties[$hash] = $dataItem;
295
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
296 2
				$this->canNotify = true;
297 2
			}
298 2
		} elseif ( $fieldChangeOp->has( 'o_id' ) ) {
299
300
			// Page object entities
301
			$oDataItem = $this->getDataItemById(
302
				$fieldChangeOp->get( 'o_id' )
303
			);
304 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...
305 2
			if ( Hooks::run( 'SMW::Notifications::CanCreateNotificationEventOnDistinctValueChange', array( $this->subject, $this->agent, $value, $oDataItem ) ) === false ) {
306 2
				return;
307 2
			}
308 2
*/
309 2
			if ( $value === str_replace( '_', ' ', $oDataItem->getDBKey() ) ) {
310 4
				$this->detectedProperties[$hash] = $dataItem;
311
				$this->subSemanticDataMatch[$hash][] = $subobjectName;
312
				$this->canNotify = true;
313
			}
314
		}
315
	}
316
317
}
318