DdsChangeLogManager   F
last analyzed

Complexity

Total Complexity 102

Size/Duplication

Total Lines 610
Duplicated Lines 0 %

Test Coverage

Coverage 92.05%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
wmc 102
eloc 277
dl 0
loc 610
rs 2
c 3
b 0
f 0
ccs 278
cts 302
cp 0.9205

26 Methods

Rating   Name   Duplication   Size   Complexity  
A listObjectHistory() 0 4 1
A isEquivalentChoice() 0 9 6
A getLastActionFromLog() 0 8 3
A listClassesWithChangeLog() 0 6 1
A addComment() 0 21 3
B calculateMinimumFieldsChangedDuringEdit() 0 21 7
A listObjectChangeLog() 0 3 1
A clearChangeLog() 0 15 4
B addLogEntry() 0 63 10
A getClassFromObject() 0 10 3
A isEquivalentBoolean() 0 3 2
B restoreObjectToLogPoint() 0 29 8
A convertFromDb() 0 5 1
A getObjectAtLogPoint() 0 7 2
B isEquivalentNull() 0 5 8
A calculateObjectAtRestorePoint() 0 15 4
A clearObjectChangeLog() 0 8 1
B getChangeLog() 0 42 9
A addGeneralComment() 0 22 3
A whoDunnit() 0 9 2
A hasChangeLog() 0 6 2
A listChangeLog() 0 3 1
A getLogEntry() 0 11 3
A getClass() 0 20 5
A setChangeLog() 0 24 4
B convertChangeLogToHistory() 0 34 8

How to fix   Complexity   

Complex Class

Complex classes like DdsChangeLogManager often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DdsChangeLogManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.newicon.net/neon
4
 * @copyright Copyright (c) 2016 Newicon Ltd
5
 * @license http://www.newicon.net/neon/license/
6
 */
7
8
namespace neon\daedalus\services\ddsChangeLog;
9
10
use neon\daedalus\interfaces\IDdsChangeLogManagement;
11
use neon\daedalus\services\ddsManager\DdsCore;
12
use neon\daedalus\services\ddsManager\models\DdsClass;
13
use neon\core\helpers\Hash;
14
use neon\core\helpers\Iterator;
15
16
17
class DdsChangeLogManager extends DdsCore
18
implements IDdsChangeLogManagement
19
{
20
	/**
21
	 * The allowed change log actions. This is a subset of allowed
22
	 * change log entries, being those ones that resulted in a change in
23
	 * an object. So this doesn't include comments for example
24
	 * @var array
25
	 */
26
	private $changeLogEntryActions = [
27
		'ADD', 'EDIT', 'DELETE', 'UNDELETE', 'DESTROY', 'RESTORE'
28
	];
29
30
	// ---------- IDdsChangeLogManagement methods ---------- //
31
32
	/**
33
	 * @inheritdoc
34
	 */
35
	public function listClassesWithChangeLog()
36
	{
37
		return DdsClass::find()
38
			->where(['change_log'=>1, 'deleted'=>0])
39
			->asArray()
40
			->all();
41
	}
42
43
	/**
44
	 * @inheritdoc
45
	 */
46 2
	public function hasChangeLog($classType)
47
	{
48 2
		if ($this->findClass($classType, $class)) {
49 2
			return (boolean)$class->hasChangeLog();
50
		}
51 2
		return false;
52
	}
53
54
	/**
55
	 * @inheritdoc
56
	 */
57 14
	public function setChangeLog($classType, $to)
58
	{
59 14
		$to = (boolean)$to;
60 14
		if ($this->findClass($classType, $class)) {
61 14
			$hasChangeLog = (boolean)$class->hasChangeLog();
62
			// only change if different to current
63 14
			if ($hasChangeLog !== $to) {
64 12
				neon('dds')->IDdsClassManagement->editClass($classType, ['change_log'=>$to]);
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsClassManagement does not exist on neon\core\ApplicationWeb. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
The method editClass() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

64
				neon('dds')->IDdsClassManagement->/** @scrutinizer ignore-call */ 
65
                                      editClass($classType, ['change_log'=>$to]);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
65
66
				// create a comment to note the change log setting
67 12
				$whoUid = $name = null;
68 12
				$this->whoDunnit($whoUid, $name);
69 12
				$date = date('D, jS M Y \a\t H:i:s');
70 12
				$description = "Change log for $classType was turned %s by $name on $date";
71 12
				neon()->db->createCommand()->insert('{{dds_change_log}}', [
72 12
					'log_uuid' => Hash::uuid64(),
73 12
					'class_type' => $class['class_type'],
74 12
					'change_key' => 'COMMENT',
75 12
					'description' => sprintf($description, ($to?'ON':'OFF')),
76 12
					'who' => $whoUid,
77 12
					'when' => date('Y-m-d H:i:s'),
78 12
				])->execute();
79
			}
80 14
			$this->clearClassCache($classType);
81
		}
82 14
	}
83
84
	/**
85
	 * @inheritdoc
86
	 */
87 14
	public function listChangeLog($fromDate=null, Iterator $iterator=null)
88
	{
89 14
		return $this->getChangeLog($fromDate, $iterator, null);
90
	}
91
92
	/**
93
	 * @inheritdoc
94
	 */
95 4
	public function listObjectChangeLog($uuid, $fromDate=null, Iterator $iterator=null)
96
	{
97 4
		return $this->getChangeLog($fromDate, $iterator, $uuid);
98
	}
99
100
	/**
101
	 * @inheritdoc
102
	 */
103 2
	public function listObjectHistory($uuid, $fromDate=null, Iterator $iterator=null)
104
	{
105 2
		$changeLog = $this->getChangeLog($fromDate, $iterator, $uuid);
106 2
		return $this->convertChangeLogToHistory($uuid, $changeLog);
107
	}
108
109
	/**
110
	 * @inheritdoc
111
	 */
112 24
	public function clearChangeLog($toDate, $classList=[], $clearClassList=true)
113
	{
114 24
		$deleteConditions = '[[when]] <= :toDate';
115 24
		$bindValues = [ ':toDate'=>$toDate ];
116 24
		if (!empty($classList)) {
117 2
			$clearClause = ($clearClassList ? '' : '!');
118 2
			$deleteConditions .= " AND $clearClause([[class_type]]=:".implode(" OR [[class_type]]=:", $classList).")";
119 2
			foreach ($classList as $class) {
120 2
				$bindValues[":$class"] = $class;
121
			}
122
		}
123 24
		neon()->db->createCommand()
124 24
			->delete('{{dds_change_log}}', $deleteConditions)
125 24
			->bindValues($bindValues)
126 24
			->execute();
127 24
	}
128
129
	/**
130
	 * @inheritdoc
131
	 */
132 2
	public function clearObjectChangeLog($uuid, $toDate)
133
	{
134 2
		$deleteConditions = '[[when]] <= :toDate AND [[object_uuid]] = :uuid';
135 2
		$bindValues = [ ':toDate'=>$toDate, ':uuid' => $uuid ];
136 2
		neon()->db->createCommand()
137 2
			->delete('{{dds_change_log}}', $deleteConditions)
138 2
			->bindValues($bindValues)
139 2
			->execute();
140 2
	}
141
142
	/**
143
	 * @inheritdoc
144
	 */
145 18
	public function addLogEntry($objectUuid, $class, $changeKey, $before=[], $after=[])
146
	{
147 18
		if (!in_array($changeKey, $this->changeLogEntryActions))
148 2
			throw new \InvalidArgumentException('Invalid change key in addLogEntry. Allowed values are ['.implode(', ', $this->changeLogEntryActions));
149
150 16
		$class = $this->getClass($class);
151
152
		// check the class does have a change log
153 16
		if (!$class['change_log'])
154
			return;
155
156
		// get who this was
157 16
		$whoUid = $name = null;
158 16
		$this->whoDunnit($whoUid, $name);
159
160
		// create a generic user friendly message for the changes made
161 16
		$description = '';
162 16
		$date = date('D, jS M Y \a\t H:i:s');
163 16
		$baseDescription = "$name %s item of type '$class[label] ($class[class_type])' on $date. The item id is '$objectUuid'.";
164 16
		switch ($changeKey) {
165 16
			case 'ADD':
166 16
				$description = sprintf($baseDescription, 'added a new');
167 16
			break;
168 16
			case 'EDIT':
169 14
				$changeCount = $this->calculateMinimumFieldsChangedDuringEdit($before, $after);
170 14
				if ($changeCount == 0)
171 2
					return;
172 14
				$description = sprintf($baseDescription, 'edited an')
173 14
					." The fields changed were ['".implode("', '", array_keys($after))."'].";
174 14
			break;
175 16
			case 'DELETE':
176 16
				$description = sprintf($baseDescription, 'soft deleted an')
177 16
					." The item can be restored using an undelete within Daedalus.";
178 16
			break;
179 14
			case 'UNDELETE':
180 12
				$description = sprintf($baseDescription, 'undeleted an')
181 12
					." The item has been undeleted.";
182 12
			break;
183 14
			case 'DESTROY':
184 14
				$description = sprintf($baseDescription, 'destroyed an')
185 14
					." The item can only be recovered from the change log or database backups.";
186 14
			break;
187 2
			case 'RESTORE':
188 2
				$description = sprintf($baseDescription, 'restored an')
189 2
					." The item was restored to its value at ".$this->restorePoint;
190 2
			break;
191
		}
192
193 16
		$logUuid = Hash::uuid64();
194
195 16
		neon()->db->createCommand()->insert('{{dds_change_log}}', [
196 16
			'log_uuid' => $logUuid,
197 16
			'object_uuid' => $objectUuid,
198 16
			'class_type' => $class['class_type'],
199 16
			'change_key' => $changeKey,
200 16
			'description' => $description,
201 16
			'who' => $whoUid,
202 16
			'when' => date('Y-m-d H:i:s'),
203 16
			'before' => json_encode($before),
204 16
			'after' => json_encode($after)
205 16
		])->execute();
206
207 16
		return $logUuid;
208
	}
209
210 4
	public function getLogEntry($logUuid)
211
	{
212 4
		if ($logUuid) {
213 4
			$entry = neon()->db->createCommand("SELECT * FROM {{dds_change_log}} WHERE [[log_uuid]] = :logUuid")
214 4
				->bindParam(":logUuid", $logUuid)
215 4
				->queryOne();
216 4
			if ($entry)
217 4
				$this->convertFromDb($entry);
0 ignored issues
show
Bug introduced by
$entry of type array is incompatible with the type neon\daedalus\services\ddsChangeLog\type expected by parameter $entry of neon\daedalus\services\d...anager::convertFromDb(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

217
				$this->convertFromDb(/** @scrutinizer ignore-type */ $entry);
Loading history...
218 4
			return $entry;
219
		}
220
		return null;
221
	}
222
223
	/**
224
	 * @inheritdoc
225
	 */
226 8
	public function addComment($objectUuid, $comment)
227
	{
228
		// check this has a change log
229 8
		$class = $this->getClassFromObject($objectUuid);
230 8
		if (empty($class) || !$class['change_log'])
231 2
			return;
232
233
		// find out who did this
234 6
		$whoUid = $name = null;
235 6
		$this->whoDunnit($whoUid, $name);
236 6
		$logUuid = Hash::uuid64();
237 6
		neon()->db->createCommand()->insert('{{dds_change_log}}', [
238 6
			'log_uuid' => $logUuid,
239 6
			'object_uuid' => $objectUuid,
240 6
			'class_type'=>$class['class_type'],
241 6
			'change_key' => 'COMMENT',
242 6
			'description' => $comment,
243 6
			'who' => $whoUid,
244 6
			'when' => date('Y-m-d H:i:s')
245 6
		])->execute();
246 6
		return $logUuid;
247
	}
248
249
	/**
250
	 * @inheritdoc
251
	 */
252 2
	public function addGeneralComment($module, $classType, $comment, $objectUuid=null, array $associateObjectUuids=[])
253
	{
254
		// check we can make a comment
255 2
		if (empty($comment))
256
			return;
257
258
		// find out who did this
259 2
		$whoUid = $name = null;
260 2
		$this->whoDunnit($whoUid, $name);
261 2
		$logUuid = Hash::uuid64();
262 2
		neon()->db->createCommand()->insert('{{dds_change_log}}', [
263 2
			'log_uuid' => $logUuid,
264 2
			'module' => $module,
265 2
			'class_type'=>$classType,
266 2
			'object_uuid' => ($objectUuid ? $objectUuid : ''),
267 2
			'associated_objects' => json_encode($associateObjectUuids),
268 2
			'change_key' => 'COMMENT',
269 2
			'description' => $comment,
270 2
			'who' => $whoUid,
271 2
			'when' => date('Y-m-d H:i:s')
272 2
		])->execute();
273 2
		return $logUuid;
274
	}
275
276
	/**
277
	 * @inheritdoc
278
	 */
279 2
	public function getObjectAtLogPoint($objectUuid, $logEntryUuid)
280
	{
281 2
		$initialObject = $restorePoint = null;
282 2
		$this->calculateObjectAtRestorePoint($objectUuid, $logEntryUuid, $initialObject, $restorePoint);
283 2
		if ($restorePoint)
284 2
			return $restorePoint['object'];
285
		return [];
286
	}
287
288
	/**
289
	 * @inheritdoc
290
	 */
291 2
	public function restoreObjectToLogPoint($objectUuid, $logEntryUuid)
292
	{
293 2
		$this->calculateObjectAtRestorePoint($objectUuid, $logEntryUuid, $initialObject, $restorePoint);
294 2
		$restoreObject = $restorePoint['object'];
295
296 2
		$dds = neon('dds')->IDdsObjectManagement;
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsObjectManagement does not exist on neon\core\ApplicationWeb. Since you implemented __get, consider adding a @property annotation.
Loading history...
297
298
		// so how to restore
299 2
		$isInitialEmpty = empty($initialObject);
300 2
		$isRestoreEmpty = empty($restoreObject);
301
302 2
		if ($isInitialEmpty && $isRestoreEmpty) {
303
			// here there is nothing to restore to and it's already gone
304 2
			return;
305 2
		} else if ($isInitialEmpty && !$isRestoreEmpty) {
306
			// here object was destroyed and needs re-adding
307 2
			$dds->addObject($restorePoint['class_type'], $restoreObject);
0 ignored issues
show
Bug introduced by
The method addObject() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

307
			$dds->/** @scrutinizer ignore-call */ 
308
         addObject($restorePoint['class_type'], $restoreObject);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
308 2
		} else if (!$isInitialEmpty && $isRestoreEmpty) {
309
			// here object exists but needs to become empty so is destroyed
310
			$dds->destroyObject($objectUuid);
311
		} else {
312
			// here we can return the object to the restore point and then
313
			// make sure it is undeleted (assuming this is what restoring requires)
314 2
			$dds->undeleteObject($objectUuid);
315 2
			$dds->editObject($objectUuid, $restoreObject);
316
		}
317
		// and make a log entry
318 2
		if (!empty($restorePoint))
319 2
			$this->addLogEntry($objectUuid, $restorePoint['class_type'], 'RESTORE', $initialObject, $restorePoint);
320 2
	}
321
322
323
	// ---------- Private Parameters and Methods ---------- //
324
325
	private $restorePoint = null;
326
327 20
	private function whoDunnit(&$who, &$name)
328
	{
329 20
		$who = null;
330 20
		$name = 'The system';
331
		// if we're a web instance we can have users
332 20
		if (neon() instanceof \yii\web\Application) {
333
			$user = neon()->user;
334
			$who = $user->uuid;
0 ignored issues
show
Bug Best Practice introduced by
The property uuid does not exist on neon\user\services\User. Since you implemented __get, consider adding a @property annotation.
Loading history...
335
			$name = $user->name;
336
		}
337 20
	}
338
339
	/**
340
	 * Get the change log given a set of criteria
341
	 * @param date $fromDate  when to start the change log from
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsChangeLog\date was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
342
	 * @param Iterator $iterator  the iterator over the change log
343
	 *   This ignored if you are getting from a particular log id
344
	 * @param uuid $objectUuid  the object you want the change log for
345
	 * @param uuid $logId  if set then get all entries from this point up
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsChangeLog\uuid was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
346
	 *   This overrides any restrictions on the iterator
347
	 * @return array  the change log
348
	 * @throws \RuntimeException  if the restore point could not be reached
349
	 *   using a sensible number of change log points (<=1000).
350
	 */
351 18
	private function getChangeLog($fromDate, $iterator, $objectUuid, $logId=null)
352
	{
353 18
		$fromDate = ($fromDate === null ? date('Y-m-d 00:00:00') : $fromDate);
354 18
		$iterator = ($iterator === null ? new Iterator : $iterator);
355
356
		// set up the basic query
357 18
		$query = (new \yii\db\Query())
358 18
			->from('dds_change_log')
359 18
			->where(['>=', 'when', $fromDate])
360 18
			->orderBy(['log_id'=>SORT_DESC]);
361
362
		// check for a particular object if required
363 18
		if ($objectUuid)
0 ignored issues
show
introduced by
$objectUuid is of type neon\daedalus\services\ddsChangeLog\uuid, thus it always evaluated to true.
Loading history...
364 6
			$query->andWhere(['object_uuid'=>$objectUuid]);
365
366
		// start from a particular log id point
367 18
		if ($logId) {
368 2
			$query->andWhere(['>=', 'log_id', $logId]);
369 2
			$iterator->start = 0;
0 ignored issues
show
Bug Best Practice introduced by
The property start does not exist on neon\core\helpers\Iterator. Since you implemented __set, consider adding a @property annotation.
Loading history...
370 2
			$iterator->length = Iterator::MAX_LENGTH;
0 ignored issues
show
Bug Best Practice introduced by
The property length does not exist on neon\core\helpers\Iterator. Since you implemented __set, consider adding a @property annotation.
Loading history...
371
		}
372
373
		// and check sensible limits
374 18
		$query->limit($iterator->length)
0 ignored issues
show
Bug Best Practice introduced by
The property length does not exist on neon\core\helpers\Iterator. Since you implemented __get, consider adding a @property annotation.
Loading history...
375 18
			->offset($iterator->start);
0 ignored issues
show
Bug Best Practice introduced by
The property start does not exist on neon\core\helpers\Iterator. Since you implemented __get, consider adding a @property annotation.
Loading history...
376 18
		if ($iterator->shouldReturnTotal())
377
			$iterator->total = $query->count();
0 ignored issues
show
Documentation Bug introduced by
It seems like $query->count() can also be of type string. However, the property $total is declared as type integer. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
378
379 18
		$changeLog = $query->all();
380
381
		// ensure we got the logId if we were looking for one
382 18
		if ($logId) {
383 2
			$last = end($changeLog);
384 2
			if ($last['log_id'] != $logId) {
385
				throw new \RuntimeException("Log restore point could not be reached from the change log.");
386
			}
387
		}
388
389 18
		foreach ($changeLog as &$entry) {
390 16
			$this->convertFromDb($entry);
391
		}
392 18
		return $changeLog;
393
	}
394
395
	/**
396
	 *
397
	 * @param type $entry
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsChangeLog\type was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
398
	 */
399 18
	private function convertFromDb(&$entry)
400
	{
401 18
		$entry['before'] = json_decode($entry['before'],true);
402 18
		$entry['after'] = json_decode($entry['after'],true);
403 18
		$entry['associated_objects'] = json_decode($entry['associated_objects'],true);
404 18
	}
405
406
	/**
407
	 * Convert a change log into a history of the state of the object at
408
	 * various points in time after each action was performed on it.
409
	 *
410
	 * @param string $uuid
411
	 * @param array $log
412
	 * @param array &$currentObject  the value of the current object
413
	 * @return array  a history log
414
	 */
415 4
	private function convertChangeLogToHistory($uuid, $log, &$currentObject=[])
416
	{
417
		// get the current object and work backwards from there to get history
418
		// first see if we can the object and if not check that the last action
419
		// was a destroy. If not then there is something wrong with the logs
420 4
		$currentObject = neon('dds')->IDdsObjectManagement->getObject($uuid);
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsObjectManagement does not exist on neon\core\ApplicationWeb. Since you implemented __get, consider adding a @property annotation.
Loading history...
Bug introduced by
The method getObject() does not exist on null. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

420
		/** @scrutinizer ignore-call */ 
421
  $currentObject = neon('dds')->IDdsObjectManagement->getObject($uuid);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
421 4
		$lastAction = $this->getLastActionFromLog($log);
422 4
		if ($currentObject == null) {
423 4
			$currentObject = [];
424 4
			if (!($lastAction == 'DESTROY' || $lastAction == 'RESTORE'))
425
				return [];
426
		}
427 4
		$object = $currentObject;
428 4
		$objectHistory = [];
429
430
		// ok so it should be possible to restore backwards from here
431 4
		foreach ($log as $l) {
432
			// set the object into an appropriate state.
433 4
			$objectHistory[] = [
434 4
				'when' => $l['when'],
435 4
				'change_key' => $l['change_key'],
436 4
				'description' => $l['description'],
437 4
				'class_type' => $l['class_type'],
438 4
				'object' => $object
439
			];
440 4
			if (is_array($l['before'])) {
441 4
				switch ($l['change_key']) {
442 4
					case 'RESTORE': $object = $l['before']; break;
443 4
					case 'ADD': $object = []; break;
444 4
					default: $object = array_merge($object, $l['before']); break;
445
				}
446
			}
447
		}
448 4
		return $objectHistory;
449
	}
450
451
	/**
452
	 * Go back through a log and find the last action. An action is one of
453
	 * ADD, EDIT, DELETE, UNDELETE, DESTROY, RESTORE
454
	 * @return string  the last valid action
455
	 */
456 4
	private function getLastActionFromLog($log)
457
	{
458 4
		foreach ($log as $l) {
459 4
			if (!in_array($l['change_key'], $this->changeLogEntryActions))
460
				continue;
461 4
			return $l['change_key'];
462
		}
463
		return null;
464
	}
465
466
	/**
467
	 * Get the class of an object
468
	 *
469
	 * @param uuid $objectUuid  the uuid of the object you want the class for
470
	 * @return string  the class type
471
	 *
472
	 * @staticvar array $objectClasses  stores previously found results
473
	 */
474 8
	private function getClassFromObject($objectUuid)
475
	{
476 8
		static $objectClasses = [];
477 8
		if (!isset($objectClasses[$objectUuid])) {
478 8
			$object = neon()->db->createCommand(
479 8
				"SELECT [[_class_type]] FROM {{dds_object}} WHERE [[_uuid]] = :objUuid"
480 8
			)->bindValue(':objUuid', $objectUuid)->queryOne();
481 8
			$objectClasses[$objectUuid] = $object ? $object['_class_type'] : '';
482
		}
483 8
		return $this->getClass($objectClasses[$objectUuid]);
484
	}
485
486
	/**
487
	 * Get hold of the class for a given class type
488
	 *
489
	 * @param array|string $class  if an array this is passed back. If string, class is
490
	 *   looked up and then returned.
491
	 * @return array
492
	 * @throws \InvalidArgumentException  if the class is neither an array or a string
493
	 *
494
	 * @staticvar array $classes  stores previously found classes
495
	 * @staticvar neon\daedalus\interfaces\IDdsClassManagement $ddc  stores reference to IDdsClassManagement
496
	 */
497 18
	private function getClass($class)
498
	{
499 18
		static $classes = [];
500 18
		static $ddc = null;
501
502
		// see if this is already an array in which case no need to look up
503 18
		if (is_array($class))
504 16
			return $class;
505
506
		// otherwise check this is a string for a class type
507 8
		if (!is_string($class))
0 ignored issues
show
introduced by
The condition is_string($class) is always true.
Loading history...
508
			throw new \InvalidArgumentException("Invalid type of class passed in. Should be either a string or an array. Type passed in was ".gettype($class));
509
510
		// and get hold of the class
511 8
		if (!isset($classes[$class])) {
512 8
			if (empty($ddc))
513 2
				$ddc = neon('dds')->IDdsClassManagement;
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsClassManagement does not exist on neon\core\ApplicationWeb. Since you implemented __get, consider adding a @property annotation.
Loading history...
514 8
			$classes[$class] = $ddc->getClass($class);
515
		}
516 8
		return $classes[$class];
517
	}
518
519
	/**
520
	 * Calculate the minimum number of changed fields made during an edit
521
	 */
522
523
	/**
524
	 * Calculate the minimum number of fields actually changed during an edit
525
	 *
526
	 * @param array &$before  the fields passed in as before the edit. Only actually changed fields
527
	 *   remain after the call. Unchanged fields are removed.
528
	 * @param array $after  the fields passed in as after the edit. Only actually changed fields
529
	 *   remain after the call. Unchanged fields are removed.
530
	 * @return boolean  true if there are any actual changes
531
	 */
532 14
	private function calculateMinimumFieldsChangedDuringEdit(&$before, &$after)
533
	{
534 14
		$beforeChangedFields = [];
535 14
		$afterChangedFields = [];
536 14
		foreach ($after as $k=>$v) {
537 14
			if (array_key_exists($k, $before)) {
538 14
				$b = $before[$k];
539
				// check for a few expected changes that aren't actual changes
540 14
				if ($this->isEquivalentNull($b, $v) || $this->isEquivalentBoolean($b, $v) || $this->isEquivalentChoice($b, $v))
541
					continue;
542 14
				if ($v !== $before[$k]) {
543 14
					$beforeChangedFields[$k] = $b;
544 14
					$afterChangedFields[$k] = $v;
545
				}
546
			} else {
547
				$afterChangedFields = $v;
548
			}
549
		}
550 14
		$before = $beforeChangedFields;
551 14
		$after = $afterChangedFields;
552 14
		return count($after)>0;
553
	}
554
555
	/**
556
	 * Calculate the object value at both the current and restore points
557
	 * @param string $objectUuid  the object you want
558
	 * @param string $logEntryUuid  the log point you want to check to
559
	 * @param array $initialObject  the current object data
560
	 * @param array $restorePoint  the final object data
561
	 * @throws \InvalidArgumentException
562
	 */
563 2
	private function calculateObjectAtRestorePoint($objectUuid, $logEntryUuid, &$initialObject, &$restorePoint)
564
	{
565
		// find the log entry point and then all entries up to and equal that one
566 2
		$restorePoint = neon()->db->createCommand("SELECT * FROM dds_change_log WHERE log_uuid = :log_uuid")
567 2
			->bindValue(':log_uuid', $logEntryUuid)
568 2
			->queryOne();
569 2
		if (!$restorePoint)
570
			throw new \InvalidArgumentException("The requested log point ($logEntryUuid) doesn't exist.");
571 2
		if ($objectUuid != $restorePoint['object_uuid'])
572
			throw new \InvalidArgumentException("The requested object ($objectUuid) and the object at the log point ($logEntryUuid) don't match");
573
574 2
		$initialObject = null;
575 2
		$changeLog = $this->getChangeLog($restorePoint['when'], null, $objectUuid, $restorePoint['log_id']);
0 ignored issues
show
Bug introduced by
$objectUuid of type string is incompatible with the type neon\daedalus\services\ddsChangeLog\uuid expected by parameter $objectUuid of neon\daedalus\services\d...Manager::getChangeLog(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

575
		$changeLog = $this->getChangeLog($restorePoint['when'], null, /** @scrutinizer ignore-type */ $objectUuid, $restorePoint['log_id']);
Loading history...
576 2
		$objectHistory = $this->convertChangeLogToHistory($objectUuid, $changeLog, $initialObject);
577 2
		$restorePoint = empty($objectHistory) ? [] : end($objectHistory);
578 2
	}
579
580
	/**
581
	 * Check if the before and after are actually the same choice. A
582
	 * choice is the same if the keys match.
583
	 *
584
	 * @param mixed $before
585
	 * @param mixed $after
586
	 * @return boolean  returns true if the before is an array of key, value, type,
587
	 *   the after is a string, the type is choice and the keys are equal
588
	 */
589 14
	private function isEquivalentChoice($before, $after)
590
	{
591
		return (
592 14
			is_array($before)
593 14
			&& array_key_exists('key', $before)
594 14
			&& array_key_exists('value', $before)
595 14
			&& array_key_exists('type', $before)
596 14
			&& $before['type'] == 'choice'
597 14
			&& $before['key'] == $after
598
		);
599
	}
600
601
	/**
602
	 * Check if the before and after values are equivalent booleans
603
	 *
604
	 * @param mixed $before
605
	 * @param mixed $after
606
	 * @return boolean  returns true if the before and after are both
607
	 *   boolean equivalents - e.g. before is true and after is 1
608
	 */
609 14
	private function isEquivalentBoolean($before, $after)
610
	{
611 14
		return (is_bool($before) && (bool)($after) == $before);
612
	}
613
614
	/**
615
	 * Check if the before and after values are equivalent nulls
616
	 *
617
	 * @param mixed $before
618
	 * @param mixed $after
619
	 * @return boolean  returns true if the before and after are both
620
	 *   null equivalents - i.e. null, empty string or empty array.
621
	 */
622 14
	private function isEquivalentNull($before, $after)
623
	{
624 14
		if ($before === null || $before === '' || (is_array($before) && count($before)==0))
625
			return ($after === null || $after === '' || (is_array($after) && count($after)==0));
626 14
		return false;
627
	}
628
629
}
630