Completed
Push — master ( c2ef24...f9d865 )
by mw
8s
created

ChangeNotifications::hasChangeToNotifAbout()   C

Complexity

Conditions 7
Paths 6

Size

Total Lines 42
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 7

Importance

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