DdsObjectManager   F
last analyzed

Complexity

Total Complexity 223

Size/Duplication

Total Lines 1378
Duplicated Lines 0 %

Test Coverage

Coverage 93.06%

Importance

Changes 8
Bugs 0 Features 0
Metric Value
eloc 653
c 8
b 0
f 0
dl 0
loc 1378
ccs 630
cts 677
cp 0.9306
rs 1.947
wmc 223

49 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectMap() 0 3 1
B destroyObjects() 0 52 10
A runSingleObjectRequest() 0 16 2
B listObjects() 0 46 11
A getLastCommitData() 0 3 2
D addObject() 0 68 12
A makeMapLookupRequest() 0 3 1
B destroyObject() 0 36 8
A undeleteObject() 0 21 6
A makeMapChainLookupRequest() 0 3 1
A getMapLookupResults() 0 3 1
A clearMapRequests() 0 3 1
A getMapChainLookupResults() 0 3 1
A commitMapRequests() 0 3 1
A getObjectFromId() 0 4 1
B convertRequestToSql() 0 31 7
A pushRequests() 0 4 1
A setMemberLinks() 0 3 1
A addObjectRequest() 0 22 1
A getObjectMapManager() 0 5 2
A createChangeLogEntry() 0 6 2
A clearRequests() 0 4 1
A checkUuidsAgainstClassType() 0 17 2
C setMembersLinks() 0 53 13
B deleteObjects() 0 46 10
A getObjectCounts() 0 7 2
A extractJoinClause() 0 10 3
A popRequests() 0 5 1
A getMemberLinks() 0 4 1
A hasObjectRequests() 0 3 1
B prepareForPDO() 0 31 9
A getMembersLinks() 0 20 4
A createSqlAndStoreRequest() 0 13 3
A extractLimitClause() 0 5 2
A getObjectRowsFromIds() 0 5 1
D editObjects() 0 79 18
A extractOrderClause() 0 18 5
C editObject() 0 69 12
B addObjects() 0 46 8
B commitRequests() 0 73 11
A onlyWantDeleted() 0 3 6
A getObjectRequests() 0 5 1
A recalculateObjectCount() 0 7 1
A extractWhereClause() 0 10 4
A convertResultsToPHP() 0 14 5
A clearCaches() 0 5 1
A deleteObject() 0 31 6
A getObject() 0 12 3
C createFilterClause() 0 76 16

How to fix   Complexity   

Complex Class

Complex classes like DdsObjectManager 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 DdsObjectManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * @link http://www.newicon.net/neon
5
 * @copyright Copyright (c) 2016 Newicon Ltd
6
 * @license http://www.newicon.net/neon/license/
7
 */
8
9
namespace neon\daedalus\services\ddsManager;
10
11
use neon\daedalus\events\ObjectEditEvent;
12
use neon\daedalus\events\ObjectAddEvent;
13
use neon\daedalus\interfaces\IDdsObjectManagement;
14
use neon\daedalus\services\ddsManager\models\DdsClass;
15
use neon\daedalus\services\ddsManager\models\DdsObject;
16
use neon\daedalus\services\ddsManager\models\DdsLink;
17
18
class DdsObjectManager
19
extends DdsCore
20
implements IDdsObjectManagement
21
{
22
23
	/** ---------------------------------------------------- */
24
	/** ---------- interface IDdsObjectManagement ---------- */
25
	/** ---------------------------------------------------- */
26
27
	/**
28
	 * Caches for results from queries so can protect against over-zealous
29
	 * requests to the database within one call
30
	 * @var array
31
	 */
32
	private static $_requestResultsCache = [];
33
	private static $_linkResultsCache = [];
34
	private static $_mapResultsCache = [];
35
36
	/**
37
	 * @inheritdoc
38
	 */
39 16
	public function addObjectRequest($classType, array $filters = [], array $order = [], array $limit = [], $resultKey = null, $includeDeleted = false)
40
	{
41
		// protect ourselves against SQL injection by making sure all
42
		// items are canonicalised as required (and use of PDO later)
43 16
		$classType = $this->canonicaliseRef($classType);
44 16
		$filters = $this->canonicaliseFilters($filters);
45 16
		$order = $this->canonicaliseOrder($order);
46 16
		$limit = $this->canonicaliseLimit($limit, $total, $calculateTotal);
47
48
		$request = [
49 16
			'classType' => $classType,
50 16
			'filters' => $filters,
51 16
			'order' => $order,
52 16
			'limit' => $limit,
53 16
			'total' => $total,
54 16
			'calculateTotal' => $calculateTotal,
55 16
			'deleted' => $includeDeleted
56
		];
57
		// add a request key so we can cache request results more easily
58 16
		$request['requestKey'] = md5(serialize($request));
59 16
		$result = $this->createSqlAndStoreRequest($request, $resultKey);
60 10
		return $result;
61
	}
62
63
	/**
64
	 * @inheritdoc
65
	 */
66 6
	public function getObjectRequests()
67
	{
68
		return [
69 6
			'requests' => $this->objectRequests,
70 6
			'boundValues' => $this->boundValues
71
		];
72
	}
73
74
	/**
75
	 * @inheritdoc
76
	 */
77 10
	public function hasObjectRequests()
78
	{
79 10
		return !empty($this->objectRequests);
80
	}
81
82
	/**
83
	 * @inheritdoc
84
	 */
85 10
	public function commitRequests()
86
	{
87
		// return an empty array if there is nothing to do.
88 10
		if (!$this->hasObjectRequests()) {
89
			profile_end('DDS::COMMIT_REQUESTS', 'dds');
90
			return [];
91
		}
92
93 10
		profile_begin('DDS::COMMIT_REQUESTS', 'dds');
94
95
		// map resultkeys to requestKeys for caching
96 10
		$resultKey2RequestKey = [];
97
98
		// get already cached results or create the query
99 10
		$query = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $query is dead and can be removed.
Loading history...
100 10
		$sql = [];
101 10
		$data = [];
102 10
		$newData = [];
103 10
		foreach ($this->objectRequests as $requests) {
104 10
			foreach ($requests as $k => $r) {
105 10
				if (array_key_exists($r['requestKey'], self::$_requestResultsCache)) {
106 2
					$data[$k] = self::$_requestResultsCache[$r['requestKey']];
107
				} else {
108
					// set null result information about the request
109 10
					$newData[$k] = ['start' => $r['limit']['start'], 'length' => 0, 'total' => $r['total'], 'rows' => []];
110 10
					$sql[] = $r['sql'];
111 10
					if ($r['totalSql'])
112 2
						$sql[] = $r['totalSql'];
113 10
					$resultKey2RequestKey[$r['resultKey']] = $r['requestKey'];
114
				}
115
			}
116
		}
117
118
		// are there any new requests to make?
119 10
		if (count($sql)) {
120 10
			$query = implode('; ', $sql);
121
			// execute the query
122 10
			$reader = neon()->db->createCommand($query, $this->boundValues)->query();
123
			// store object ids ready to get multilinks afterwards
124 10
			$objectIds = [];
125
			// extract the results
126
			do {
127 10
				$results = $reader->readAll();
128 10
				foreach ($results as $r) {
129 10
					$resultKey = $r[$this->resultKeyField];
130 10
					$resultKey2RequestKey[$resultKey] = $r[$this->requestKeyField];
131 10
					unset($r[$this->resultKeyField]);
132 10
					if (strpos($resultKey, '_total') !== false) {
133 2
						$newData[substr($resultKey, 0, -6)]['total'] = $r['total'];
134
					} else {
135 10
						$objectIds[$r['_uuid']] = $r['_uuid'];
136 10
						$newData[$resultKey]['rows'][] = $r;
137 10
						$newData[$resultKey]['length']++;
138
					}
139
				}
140 10
			} while ($reader->nextResult());
141
142
			// get all links across the entire resultset
143 10
			$membersLinks = $this->getMembersLinks($objectIds);
144
145
			// convert any data to PHP format
146 10
			foreach ($newData as $k => &$d) {
147 10
				$this->convertResultsToPHP($d['rows'], $membersLinks);
148 10
				self::$_requestResultsCache[$resultKey2RequestKey[$k]] = $d;
149 10
				$data[$k] = $d;
150
			}
151
		}
152 10
		$this->lastCommitResults = $data;
153 10
		$this->clearRequests();
154
155 10
		profile_end('DDS::COMMIT_REQUESTS', 'dds');
156
157 10
		return $data;
158
	}
159
160
	/**
161
	 * @inheritdoc
162
	 */
163
	public function getLastCommitData($requestKey)
164
	{
165
		return isset($this->lastCommitResults[$requestKey]) ? $this->lastCommitResults[$requestKey] : [];
166
	}
167
168
	/**
169
	 * @inheritdoc
170
	 */
171 6
	public function runSingleObjectRequest($classType, array $filters = [], array $order = [], array $limit = [], $includeDeleted = false)
172
	{
173 6
		if (empty($classType))
174
			throw new \InvalidArgumentException('ClassType must be specified');
175
176
		// make sure that any currently queued requests from elsewhere are saved off the queue
177 6
		$currentRequests = $this->popRequests();
178
179
		// make the object request
180 6
		$requestId = $this->addObjectRequest($classType, $filters, $order, $limit, null, $includeDeleted);
181 6
		$data = $this->commitRequests();
182
183
		// and store them back again
184 6
		$this->pushRequests($currentRequests);
185
186 6
		return $data[$requestId];
187
	}
188
189
190
	/** ---------- Object Map Management ---------- **
191
192
	/**
193
	 * @inheritdoc
194
	 */
195 4
	public function getObjectMap($classType, $filters = [], $fields = [], $start = 0, $length = 1000, $includeDeleted = false)
196
	{
197 4
		return $this->getObjectMapManager()->getObjectMap($classType, $filters, $fields, $start, $length, $includeDeleted);
198
	}
199
200
	/**
201
	 * @inheritdoc
202
	 */
203 24
	public function makeMapLookupRequest($objectUuids, $mapFields = [], $classType = null)
204
	{
205 24
		return $this->getObjectMapManager()->makeMapLookupRequest($objectUuids, $mapFields, $classType);
206
	}
207
208
	/**
209
	 * @inheritdoc
210
	 */
211 10
	public function makeMapChainLookupRequest($objectUuids, $chainMembers = [], $chainMapFields = [], $inReverse = false)
212
	{
213 10
		return $this->getObjectMapManager()->makeMapChainLookupRequest($objectUuids, $chainMembers, $chainMapFields, $inReverse);
214
	}
215
216
217
	/**
218
	 * @inheritdoc
219
	 */
220 20
	public function commitMapRequests()
221
	{
222 20
		return $this->getObjectMapManager()->commitMapRequests();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getObjectMapManager()->commitMapRequests() targeting neon\daedalus\services\d...er::commitMapRequests() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
223
	}
224
225
	/**
226
	 * @inheritdoc
227
	 */
228 24
	public function getMapLookupResults($requestKey)
229
	{
230 24
		return $this->getObjectMapManager()->getMapLookupResults($requestKey);
231
	}
232
233
	/**
234
	 * @inheritdoc
235
	 */
236 10
	public function getMapChainLookupResults($requestKey)
237
	{
238 10
		return $this->getObjectMapManager()->getMapChainLookupResults($requestKey);
239
	}
240
241
	/**
242
	 * @inheritdoc
243
	 */
244 22
	public function clearMapRequests()
245
	{
246 22
		return $this->getObjectMapManager()->clearMapRequests();
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getObjectMapManager()->clearMapRequests() targeting neon\daedalus\services\d...ger::clearMapRequests() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
247
	}
248
249
	/** ---------- Basic Object Management ---------- **/
250
251
	/**
252
	 * @inheritdoc
253
	 */
254 18
	public function listObjects($classType, &$total = null, $includeDeleted = false, $inFull = true, $start = 0, $length = 100, $order = null)
255
	{
256 18
		$classType = $this->canonicaliseRef($classType);
257 18
		if (empty($order))
258 18
			$order = ['_created' => 'DESC'];
259 18
		$order = $this->canonicaliseOrder($order);
260
261
		// get the total first
262 18
		$class = null;
263 18
		$this->findClass($classType, $class, true);
264 18
		$total = $class->count_total;
265 18
		if (!$includeDeleted)
266 18
			$total -= $class->count_deleted;
267
268
		// now get the objects
269 18
		$query = (new \neon\core\db\Query)
270 18
			->from('{{dds_object}} o')
271 18
			->select($inFull ? "[[t]].*, [[o]].*" : "[[o]].*")
272 18
			->where(['[[o]].[[_class_type]]' => $classType])
273 18
			->offset($start)
274 18
			->limit($length);
275
276 18
		if (!$includeDeleted)
277 18
			$query->andWhere('[[o]].[[_deleted]] = 0');
278 18
		if ($inFull) {
279 18
			$tableName = $this->getTableFromClassType($classType);
280 18
			$query->innerJoin('{{' . $tableName . '}} t', '[[o]].[[_uuid]]=[[t]].[[_uuid]]');
281
		}
282
283 18
		if (is_array($order)) {
0 ignored issues
show
introduced by
The condition is_array($order) is always false.
Loading history...
284 18
			$orderBits = [];
285 18
			foreach ($order as $k => $d) {
286 18
				if ($k[1] == 'o') {
287 18
					$orderBits[] = "$k $d";
288 2
				} else if ($inFull) {
289 2
					$orderBits[] = "[[t]].$k $d";
290
				}
291
			}
292 18
			if ($orderBits)
293 18
				$orderClause = implode(', ', $orderBits);
294 18
			$query->orderBy($orderClause);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $orderClause does not seem to be defined for all execution paths leading up to this point.
Loading history...
295
		}
296
297 18
		$data = $query->all();
298 18
		$this->convertResultsToPHP($data);
299 18
		return $data;
300
	}
301
302
	/**
303
	 * @inheritdoc
304
	 */
305 66
	public function addObject($classType, array $data, &$changeLogUuid = null)
306
	{
307 66
		$class = null;
308 66
		$classType = $this->canonicaliseRef($classType);
309 66
		$this->findClass($classType, $class, true);
310 66
		if ($class->hasChangeLog())
311 8
			$newValues = $data;
312 66
		$table = $this->getTableFromClassType($classType);
313
314
		// create the new object
315 66
		$object = new DdsObject;
316 66
		$now = date('Y-m-d H:i:s');
317 66
		$uuid = (!empty($data['_uuid']) && $this->isUUID($data['_uuid'])) ? $data['_uuid'] : $this->generateUUID();
318 66
		$object->_uuid = $uuid;
0 ignored issues
show
Documentation Bug introduced by
It seems like $uuid can also be of type string[]. However, the property $_uuid is declared as type string. 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...
319 66
		$object->_class_type = $classType;
320 66
		$object->_created = $now;
321 66
		$object->_updated = $now;
322 66
		if (!$object->save())
323
			throw new \RuntimeException("Couldn't create object: " . print_r($object->errors, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($object->errors, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

323
			throw new \RuntimeException("Couldn't create object: " . /** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
324
325
		// increment the class's object count
326 66
		$class->count_total += 1;
327 66
		$class->save();
328
329
		// then insert all of the associated data
330
		try {
331 66
			$this->convertFromPHPToDB($classType, $data, $links);
332 66
			$memberStorage = $this->getMemberStorage($classType);
333 66
			$count = 0;
334 66
			$fields = [];
335 66
			$inserts = [];
336 66
			$values = [];
337 66
			foreach (array_keys($memberStorage) as $memRef) {
338
				// save all non-empty fields.
339 66
				if (array_key_exists($memRef, $data) && $data[$memRef] !== null && $data[$memRef] !== '') {
340 60
					$fields[] = "`$memRef`";
341 60
					$inserts[] = ":v$count";
342 60
					$values[":v$count"] = $data[$memRef];
343 60
					$count++;
344
				}
345
			}
346
347 66
			if (count($fields))
348 60
				$query = "INSERT INTO `$table` (`_uuid`, " . implode(', ', $fields) . ") VALUES ('{$object->_uuid}', " . implode(', ', $inserts) . ")";
349
			else
350 6
				$query = "INSERT INTO `$table` (`_uuid`) VALUES ('{$object->_uuid}')";
351 66
			$command = neon()->db->createCommand($query);
352 66
			$command->bindValues($values);
353 66
			$command->execute();
354
355
			// if there are any links then add these to the link table
356 66
			$this->setMemberLinks($object->_uuid, $links);
357
358 66
			if ($class->hasChangeLog())
359 66
				$changeLogUuid = $this->createChangeLogEntry($class, 'ADD', $object->_uuid, [], $newValues);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
It seems like $object->_uuid can also be of type string[]; however, parameter $objectUuid of neon\daedalus\services\d...:createChangeLogEntry() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

359
				$changeLogUuid = $this->createChangeLogEntry($class, 'ADD', /** @scrutinizer ignore-type */ $object->_uuid, [], $newValues);
Loading history...
360
		} catch (\Exception $e) {
361
			// execution failed so clean up the object
362
			$object->delete();
363
			$this->clearCaches();
364
			throw $e;
365
		}
366 66
		$this->clearCaches();
367
368 66
		$this->trigger("dds.afterAdd.$classType", new ObjectAddEvent([
369 66
			'classType' => $classType,
370 66
			'object' => array_merge($data, $object->toArray())
371
		]));
372 66
		return $object->_uuid;
373
	}
374
375
	/**
376
	 * @inheritdoc
377
	 */
378 4
	public function addObjects($classType, array $data, $chunkSize = 1000)
379
	{
380 4
		$classType = $this->canonicaliseRef($classType);
381 4
		$class = null;
382 4
		$this->findClass($classType, $class, true);
383 4
		$table = $this->getTableFromClassType($classType);
384 4
		$hasChangeLog = $class->hasChangeLog();
385
386
		// get a list of the columns
387 4
		$columns = [];
388 4
		$nullRow = [];
389 4
		$memberStorage = $this->getMemberStorage($classType);
390 4
		foreach (array_keys($memberStorage) as $memRef) {
391 4
			$columns[] = $memRef;
392 4
			$nullRow[$memRef] = null;
393
		}
394 4
		$columns[] = '_uuid';
395 4
		$nullRow['_uuid'] = null;
396
397 4
		foreach (array_chunk($data, $chunkSize) as $chunkData) {
398 4
			$rows = [];
399 4
			$objectDbRows = [];
400 4
			$objectsLinks = [];
401 4
			$now = date('Y-m-d H:i:s');
402 4
			foreach ($chunkData as $row) {
403 4
				if ($hasChangeLog)
404 2
					$newValues = $row;
405
				// ddt table value - format row data and append the uuid
406 4
				$this->convertFromPHPToDB($classType, $row, $links);
407 4
				$uuid = (!empty($row['_uuid']) && $this->isUUID($row['_uuid'])) ? $row['_uuid'] : $this->generateUUID();
408 4
				$row['_uuid'] = $uuid;
409
				// only include data that belongs in the table and null value remainder
410 4
				$rows[] = array_replace($nullRow, array_intersect_key($row, $nullRow));
411
				// object table row value
412 4
				$objectDbRows[] = [$row['_uuid'], $classType, $now, $now];
413 4
				$objectsLinks[$row['_uuid']] = $links;
414 4
				if ($hasChangeLog)
415 2
					$this->createChangeLogEntry($class, 'ADD', $uuid, [], $newValues);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
Bug introduced by
It seems like $uuid can also be of type string[]; however, parameter $objectUuid of neon\daedalus\services\d...:createChangeLogEntry() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

415
					$this->createChangeLogEntry($class, 'ADD', /** @scrutinizer ignore-type */ $uuid, [], $newValues);
Loading history...
416
			}
417 4
			neon()->db->createCommand()->batchInsert(DdsObject::tableName(), ['_uuid', '_class_type', '_created', '_updated'], $objectDbRows)->execute();
418 4
			neon()->db->createCommand()->batchInsert($table, $columns, $rows)->execute();
419 4
			$this->setMembersLinks($objectsLinks, $chunkSize);
420 4
			$class->count_total += count($chunkData);
421
		}
422 4
		$class->save();
423 4
		$this->clearCaches();
424 4
	}
425
426
	/**
427
	 * @inheritdoc
428
	 */
429 28
	public function getObject($uuid)
430
	{
431 28
		$object = null;
432 28
		if ($this->getObjectFromId($uuid, $object)) {
433 28
			$table = $this->getTableFromClassType($object->_class_type);
434 28
			$query = "SELECT `o`.*, `t`.* FROM `dds_object` `o` LEFT JOIN  `$table` `t` ON `o`.`_uuid`=`t`.`_uuid` WHERE `o`.`_uuid`=:uuid LIMIT 1";
435 28
			$rows = neon()->db->createCommand($query)->bindValue(':uuid', $uuid)->queryAll();
436 28
			$row =  count($rows) > 0 ? $rows[0] : null;
437 28
			$this->convertFromDBToPHP($row, $this->getMemberLinks($uuid));
438 28
			return $row;
439
		}
440 8
		return null;
441
	}
442
443
	/**
444
	 * @inheritdoc
445
	 */
446 20
	public function editObject($uuid, array $changes, &$changeLogUuid = null)
447
	{
448
		// check there are some changes to make
449 20
		if (count($changes) == 0)
450
			return false;
451
452
		// check that the object exists and find its class type
453 20
		if (!$this->getObjectFromId($uuid, $object))
454
			throw new \InvalidArgumentException("Couldn't edit object with id $uuid as not found");
455
456
		// see if we need to store the change log
457 20
		$class = null;
458 20
		$this->findClass($object['_class_type'], $class);
459
460 20
		$object->_updated = date('Y-m-d H:i:s');
461 20
		if (!$object->save())
462
			return $object->errors;
463
464
		// now update any member changes
465
		try {
466
			// note before and after values if has a change log
467 20
			$currentObject = $this->getObject($uuid);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $currentObject is correct as $this->getObject($uuid) targeting neon\daedalus\services\d...ectManager::getObject() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
468 20
			$memberStorage = array_diff_key(array_keys($currentObject), array_keys($object->attributes));
0 ignored issues
show
Bug introduced by
$currentObject of type null is incompatible with the type array expected by parameter $array of array_keys(). ( Ignorable by Annotation )

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

468
			$memberStorage = array_diff_key(array_keys(/** @scrutinizer ignore-type */ $currentObject), array_keys($object->attributes));
Loading history...
469 20
			$originalValues = array_intersect_key($currentObject, $changes);
0 ignored issues
show
Bug introduced by
$currentObject of type null is incompatible with the type array expected by parameter $array1 of array_intersect_key(). ( Ignorable by Annotation )

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

469
			$originalValues = array_intersect_key(/** @scrutinizer ignore-type */ $currentObject, $changes);
Loading history...
470 20
			$newValues = array_intersect_key($changes, array_flip($memberStorage));
471
472 20
			$this->convertFromPHPToDB($object['_class_type'], $changes, $links);
473 20
			$table = $this->getTableFromClassType($object['_class_type']);
474
475 20
			$count = 0;
476 20
			$updates = [];
477 20
			$values = [];
478 20
			foreach ($memberStorage as $memRef) {
479 20
				if (array_key_exists($memRef, $changes)) {
480
					// when saving changes, need to manage empty fields by setting to null
481
					// otherwise saving what has been provided
482 20
					if ($changes[$memRef] === null || $changes[$memRef] === '') {
483 6
						$updates[] = "`$memRef`=NULL";
484
					} else {
485 16
						$updates[] = "`$memRef`=:v$count";
486 16
						$values[":v$count"] = $changes[$memRef];
487 16
						$count++;
488
					}
489
				}
490
			}
491 20
			if (count($updates) == 0)
492
				return false;
493
494 20
			$query = "UPDATE `$table` SET " . implode(', ', $updates) . " WHERE `_uuid`='$uuid' LIMIT 1";
495 20
			neon()->db->createCommand($query)->bindValues($values)->execute();
496
497
			// add or remove any links from the linking table
498 20
			$this->setMemberLinks($object->_uuid, $links);
499
500
			// finally note the changes in the change log
501 20
			if ($class && $class->hasChangeLog())
502 20
				$changeLogUuid = $this->createChangeLogEntry($class, 'EDIT', $uuid, $originalValues, $newValues);
503
		} catch (\yii\db\exception $e) {
504
			$this->clearCaches();
505
			return ['error' => $e->getMessage()];
506
		}
507 20
		$this->clearCaches();
508
509 20
		$this->trigger("dds.afterEdit.{$object['_class_type']}", new ObjectEditEvent([
510 20
			'classType' => $object['_class_type'],
511 20
			'object' => array_merge($changes, $object->toArray()),
512 20
			'originalValues' => $originalValues
513
		]));
514 20
		return true;
515
	}
516
517
	/**
518
	 * NOTE - this method makes the same set of changes across a series of objects
519
	 * and is not a way of bulk updating objects differently
520
	 * @inheritdoc
521
	 */
522 4
	public function editObjects($classType, array $uuids, array $changes)
523
	{
524 4
		if (count($uuids) == 0 || count($changes) == 0)
525 2
			return false;
526
		try {
527 4
			$uuids = $this->checkUuidsAgainstClassType($uuids, $classType);
528 4
			if (count($uuids) == 0)
529
				return false;
530 4
			$table = $this->getTableFromClassType($classType);
531 4
			$memberStorage = $this->getMemberStorage($classType);
532 4
			if (count($memberStorage) == 0) {
533
				throw new \InvalidArgumentException('Unknown or empty class type ' . $classType);
534
			}
535
536
			// see if we are recording changes into the change log
537 4
			$class = null;
538 4
			$this->findClass($classType, $class);
539 4
			if ($class && $class->hasChangeLog()) {
540 2
				foreach ($uuids as $uuid)
541 2
					$beforeValues[$uuid] = array_intersect_key($this->getObject($uuid), $changes);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->getObject($uuid) targeting neon\daedalus\services\d...ectManager::getObject() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
Bug introduced by
$this->getObject($uuid) of type null is incompatible with the type array expected by parameter $array1 of array_intersect_key(). ( Ignorable by Annotation )

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

541
					$beforeValues[$uuid] = array_intersect_key(/** @scrutinizer ignore-type */ $this->getObject($uuid), $changes);
Loading history...
542 2
				$newValues = $changes;
543
			}
544
545
			// make the changes
546 4
			$this->convertFromPHPToDB($classType, $changes, $links);
547 4
			$count = 0;
548 4
			$updates = [];
549 4
			$values = [];
550 4
			foreach (array_keys($memberStorage) as $memRef) {
551 4
				if (array_key_exists($memRef, $changes)) {
552
					// when saving changes, need to manage empty fields by setting to null
553
					// otherwise saving what has been provided
554 4
					if ($changes[$memRef] === null || $changes[$memRef] === '') {
555
						$updates[] = "`$memRef`=NULL";
556
					} else {
557 4
						$updates[] = "`$memRef`=:v$count";
558 4
						$values[":v$count"] = $changes[$memRef];
559 4
						$count++;
560
					}
561
				}
562
			}
563 4
			if (count($updates) == 0)
564 2
				return false;
565
566
			// sort out the uuid clause for both the update on the table and in the object table
567 4
			$uuidClause = [];
568 4
			$objectValues = [];
569 4
			foreach ($uuids as $id) {
570 4
				$placeHolder = ":v$count";
571 4
				$uuidClause[] = $placeHolder;
572 4
				$values[$placeHolder] = $id;
573 4
				$objectValues[$placeHolder] = $id;
574 4
				$count++;
575
			}
576 4
			$uuidClause = '(' . implode(',', $uuidClause) . ')';
577
578
			// update the latest changed
579 4
			$db = neon()->db;
580 4
			$query = "UPDATE `dds_object` SET `_updated`=NOW() WHERE `_uuid` IN $uuidClause LIMIT " . count($uuids);
581 4
			$db->createCommand($query)->bindValues($objectValues)->execute();
582
			// make the changes
583 4
			$query = "UPDATE `$table` SET " . implode(', ', $updates) . " WHERE `_uuid` IN $uuidClause LIMIT " . count($uuids);
584 4
			$db->createCommand($query)->bindValues($values)->execute();
585
586
			//
587
			// do NOT update any member links - that would likely be a programmatic error
588
			//
589
590
			// and save entries into a change log
591 4
			if ($class && $class->hasChangeLog()) {
592 4
				foreach ($beforeValues as $uuid => $before)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $beforeValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
593 2
					$this->createChangeLogEntry($class, 'EDIT', $uuid, $before, $newValues);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $newValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
594
			}
595
		} catch (\yii\db\exception $e) {
596
			$this->clearCaches();
597
			return ['error' => $e->getMessage()];
598
		}
599 4
		$this->clearCaches();
600 4
		return true;
601
	}
602
603
	/**
604
	 * @inheritdoc
605
	 */
606 42
	public function deleteObject($uuid, &$changeLogUuid = null)
607
	{
608 42
		$object = null;
609
		// find the object and delete if it hasn't already been
610 42
		if ($this->getObjectFromId($uuid, $object) && $object->_deleted == 0) {
611 42
			$object->_deleted = 1;
612 42
			$object->_updated = date('Y-m-d H:i:s');
613 42
			if (!$object->save())
614
				throw new \RuntimeException("Couldn't delete the object: " . print_r($object->errors, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($object->errors, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

614
				throw new \RuntimeException("Couldn't delete the object: " . /** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
615
			// and update the class object deleted count
616 42
			$class = null;
617 42
			$this->findClass($object['_class_type'], $class);
618 42
			$class->count_deleted += 1;
619 42
			$class->save();
620
621
			// see if we need to store the change log
622 42
			if ($class && $class->hasChangeLog())
623 8
				$changeLogUuid = $this->createChangeLogEntry($class, 'DELETE', $uuid);
624
625
			/** ---- KEEP ME ----
626
			 * don't delete the links so any that haven't been deleted between
627
			 * deletion and undeletion can be recreated. The most likely usecase
628
			 * here is an accidental delete followed by an immediate undelete.
629
			 * As far as I can see there is no definitive solution here that covers
630
			 * all possible usecases, so this is a behavioural choice.
631
			 * See setMembersLinks for more on this choice.
632
			 *
633
			 * Links are deleted when objects are destroyed
634
			 * ---- KEEP ME ---- */
635
636 42
			$this->clearCaches();
637
		}
638 42
	}
639
640
	/**
641
	 * @inheritdoc
642
	 */
643 6
	public function deleteObjects(array $uuids)
644
	{
645 6
		$objects = $this->getObjectRowsFromIds($uuids);
646 6
		if (count($objects) == 0)
647
			return;
648
649 6
		$foundUuids = [];
650 6
		$foundClasses = [];
651 6
		$deletedUuids = [];
652 6
		foreach ($objects as $obj) {
653 6
			if ($obj['_deleted'] == 0) {
654 6
				$foundUuids[$obj['_uuid']] = $obj['_uuid'];
655 6
				$classType = $obj['_class_type'];
656 6
				if (!isset($foundClasses[$classType])) {
657 6
					$foundClasses[$classType] = 1;
658
				} else {
659 4
					$foundClasses[$classType] += 1;
660
				}
661 6
				$deletedUuids[$classType][$obj['_uuid']] = $obj['_uuid'];
662
			}
663
		}
664 6
		if (count($foundUuids))
665 6
			DdsObject::updateAll(['_deleted' => 1, '_updated' => date('Y-m-d H:i:s')], ['_uuid' => $foundUuids]);
666 6
		foreach ($foundClasses as $type => $count) {
667 6
			$class = null;
668 6
			if ($this->findClass($type, $class)) {
669 6
				$class->count_deleted += $count;
670 6
				$class->save();
671
				// TODO 20200107 Make a bulk call for change log entries
672 6
				if ($class->hasChangeLog()) {
673 2
					foreach ($deletedUuids[$class->class_type] as $objUuid)
674 2
						$this->createChangeLogEntry($class, 'DELETE', $objUuid);
675
				}
676
			}
677
		}
678
		/** ---- KEEP ME ----
679
		 * don't delete the links so any that haven't been deleted between
680
		 * deletion and undeletion can be recreated. The most likely usecase
681
		 * here is an accidental delete followed by an immediate undelete.
682
		 * As far as I can see there is no definitive solution here that covers
683
		 * all possible usecases, so this is a behavioural choice.
684
		 * See setMembersLinks for more on this choice.
685
		 *
686
		 * Links are deleted when objects are destroyed
687
		 * ---- KEEP ME ---- */
688 6
		$this->clearCaches();
689 6
	}
690
691
	/**
692
	 * @inheritdoc
693
	 */
694 20
	public function undeleteObject($uuid, &$changeLogUuid = null)
695
	{
696 20
		$object = null;
697
		// find the object and delete if it hasn't already been
698 20
		if ($this->getObjectFromId($uuid, $object) && $object->_deleted == 1) {
699 20
			$object->_deleted = 0;
700 20
			$object->_updated = date('Y-m-d H:i:s');
701 20
			if (!$object->save())
702
				throw new \RuntimeException("Couldn't undelete the object: " . print_r($object->errors, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($object->errors, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

702
				throw new \RuntimeException("Couldn't undelete the object: " . /** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
703
704
			// and update the class object deleted count
705 20
			$class = null;
706 20
			if ($this->findClass($object['_class_type'], $class)) {
707 20
				$class->count_deleted = max(0, $class->count_deleted - 1);
708 20
				$class->save();
709
710
				// see if we need to store the change log
711 20
				if ($class->hasChangeLog())
712 6
					$changeLogUuid = $this->createChangeLogEntry($class, 'UNDELETE', $uuid);
713
			}
714 20
			$this->clearCaches();
715
		}
716 20
	}
717
718
	/**
719
	 * @inheritdoc
720
	 */
721 38
	public function destroyObject($uuid, &$changeLogUuid = null)
722
	{
723 38
		$object = null;
724 38
		if ($this->getObjectFromId($uuid, $object)) {
725 38
			$class = null;
726 38
			$this->findClass($object['_class_type'], $class);
727 38
			if ($class && $class->hasChangeLog())
728 6
				$originalValues = $this->getObject($uuid);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $originalValues is correct as $this->getObject($uuid) targeting neon\daedalus\services\d...ectManager::getObject() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
729
730
			// delete any object table data
731 38
			$table = $this->getTableFromClassType($object['_class_type']);
732 38
			neon()->db->createCommand()
733 38
				->delete($table, ['_uuid' => $uuid])
734 38
				->execute();
735
736
			// and the object row itself
737 38
			neon()->db->createCommand()
738 38
				->delete('dds_object', ['_uuid' => $uuid])
739 38
				->execute();
740
741
			// then if ok, decrement the class's object counts
742 38
			if ($class) {
743 38
				$class->count_total = max(0, $class->count_total - 1);
744 38
				if ($object['_deleted'] == 1)
745 20
					$class->count_deleted = max(0, $class->count_deleted - 1);
746 38
				$class->save();
747
			}
748
749
			// finally, delete any links from or to the object
750 38
			DdsLink::deleteAll(['or', ['from_id' => $uuid], ['to_id' => $uuid]]);
751
752
			// and note this is in the change log
753 38
			if ($class && $class->hasChangeLog())
754 6
				$changeLogUuid = $this->createChangeLogEntry($class, 'DESTROY', $uuid, $originalValues);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $originalValues does not seem to be defined for all execution paths leading up to this point.
Loading history...
755
756 38
			$this->clearCaches();
757
		}
758 38
	}
759
760
	/**
761
	 * @inheritdoc
762
	 */
763 6
	public function destroyObjects(array $uuids)
764
	{
765 6
		$objects = $this->getObjectRowsFromIds($uuids);
766 6
		if (count($objects) == 0)
767
			return;
768
769 6
		$objectUuids = [];
770 6
		$objectsDeleted = [];
771 6
		$classUuids = [];
772 6
		foreach ($objects as $obj) {
773 6
			$objectUuids[$obj['_uuid']] = $obj['_uuid'];
774 6
			if ($obj['_deleted'] == 1)
775 6
				$objectsDeleted[$obj['_class_type']][$obj['_uuid']] = $obj['_uuid'];
776 6
			$classUuids[$obj['_class_type']][] = $obj['_uuid'];
777
		}
778
779
		// delete all of the objects from their appropriate class table
780 6
		foreach ($classUuids as $classType => $uuids) {
781 6
			$class = null;
782 6
			$this->findClass($classType, $class);
783 6
			$table = $this->getTableFromClassType($classType);
784
785
			// note the destroys in the change log
786
			// TODO 20200107 Make a bulk call for change log entries
787 6
			if ($class && $class->hasChangeLog()) {
788 2
				foreach ($uuids as $uuid) {
789 2
					$data = $this->getObject($uuid);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $data is correct as $this->getObject($uuid) targeting neon\daedalus\services\d...ectManager::getObject() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
790 2
					$this->createChangeLogEntry($class, 'DESTROY', $uuid, $data);
791
				}
792
			}
793
794
			// now delete all of the objects
795 6
			neon()->db->createCommand()
796 6
				->delete($table, ['_uuid' => $uuids])
797 6
				->execute();
798
799
			// decrement the class's object counts
800 6
			if ($class) {
801 6
				$class->count_total = max(0, $class->count_total - count($uuids));
802 6
				if (isset($objectsDeleted[$classType]))
803 6
					$class->count_deleted = max(0, $class->count_deleted - count($objectsDeleted[$classType]));
804 6
				$class->save();
805
			}
806
		}
807
808
		// delete all of the objects from the DdsObject table
809 6
		DdsObject::deleteAll(['_uuid' => $objectUuids]);
810
811
		// finally, delete any links from or to the objects
812 6
		DdsLink::deleteAll(['or', ['from_id' => $objectUuids], ['to_id' => $objectUuids]]);
813
814 6
		$this->clearCaches();
815 6
	}
816
817
	/**
818
	 * @inheritdoc
819
	 */
820 2
	public function getObjectCounts($classType)
821
	{
822 2
		$row = DdsClass::find()->where(['class_type'=>$classType])->one()->toArray();
823 2
		if ($row) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $row of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
824 2
			return ['total'=>$row['count_total'], 'active'=>($row['count_total']-$row['count_deleted']), 'deleted'=>$row['count_deleted']];
825
		}
826
		return null;
827
	}
828
829
830
	/**
831
	 * @inheritdoc
832
	 */
833 2
	public function recalculateObjectCount($classType)
834
	{
835
		$query = <<<EOQ
836 2
		UPDATE dds_class SET count_total = (SELECT COUNT(*) FROM dds_object WHERE _class_type=:CLASSTYPE1),
837
		count_deleted = (SELECT COUNT(*) FROM dds_object WHERE _class_type=:CLASSTYPE2 AND _deleted=1) LIMIT 1;
838
EOQ;
839 2
		neon()->db->createCommand($query, ['CLASSTYPE1'=>$classType, 'CLASSTYPE2'=>$classType])->execute();
840 2
	}
841
842
	/** -------------------------------------- **/
843
	/** ---------- Internal Methods ---------- **/
844
845
	/**
846
	 * @var \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
847
	 */
848
	private $_objectMapManager = null;
849
850
	/**
851
	 * @return \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
852
	 */
853 28
	private function getObjectMapManager()
854
	{
855 28
		if (!$this->_objectMapManager)
856 28
			$this->_objectMapManager = new DdsObjectMapManager;
857 28
		return $this->_objectMapManager;
858
	}
859
860
	/**
861
	 * convert a row of data from the database to php
862
	 * @param [] &$dataSet  [n]=>$rows  rows of requested data
0 ignored issues
show
Documentation Bug introduced by
The doc comment [] at position 0 could not be parsed: Unknown type name '[' at position 0 in [].
Loading history...
863
	 * @param string $classTypeKey the key in the data that will give the class type
864
	 */
865 22
	private function convertResultsToPHP(&$dataSet, $links = null, $classTypeKey = '_class_type')
866
	{
867
		// if we haven't been provided any multilinks (even if empty array)
868
		// then see if there are any multilinks to extract
869 22
		if ($links === null) {
870 18
			$objectIds = [];
871 18
			foreach ($dataSet as $row)
872 14
				$objectIds[] = $row['_uuid'];
873 18
			$links = $this->getMembersLinks($objectIds);
874
		}
875 22
		foreach ($dataSet as &$data) {
876
			// convert full rows to database. Totals are left untouched
877 18
			if (isset($data[$classTypeKey]))
878 18
				$this->convertFromDBToPHP($data, $links[$data['_uuid']], $classTypeKey);
879
		}
880 22
	}
881
882
	/**
883
	 * Get the model object from its id
884
	 * @param string $uuid  the object id
885
	 * @param DdsObject &$object  the returned object
886
	 * @return boolean  whether or not object found
887
	 */
888 52
	private function getObjectFromId($uuid, &$object = null)
889
	{
890 52
		$object = DdsObject::find()->noCache()->where(['_uuid' => $uuid])->one();
891 52
		return ($object !== null);
892
	}
893
894
	/**
895
	 * Get the array data for a row from its id
896
	 * @param array $uuids  an array of object uuids
897
	 * @return array  the objects found
898
	 */
899 8
	private function getObjectRowsFromIds(array $uuids)
900
	{
901 8
		$uuids = array_unique($uuids);
902 8
		$rows = DdsObject::find()->noCache()->where(['_uuid' => $uuids])->asArray()->all();
903 8
		return $rows;
904
	}
905
906
	/**
907
	 * @var array  values bound for the next commit
908
	 */
909
	private $boundValues = [];
910
911
	/**
912
	 * @var string  internal name for request keys
913
	 */
914
	private $requestKeyField = "___request_key";
915
916
	/**
917
	 * @var string  internal name for result keys
918
	 */
919
	private $resultKeyField = "___result_key";
920
921
	/**
922
	 * @var array  store of the last commit
923
	 */
924
	private $lastCommitResults = [];
925
926
	/**
927
	 * @var [] the set of object requests
0 ignored issues
show
Documentation Bug introduced by
The doc comment [] at position 0 could not be parsed: Unknown type name '[' at position 0 in [].
Loading history...
928
	 */
929
	private $objectRequests = [];
930
931
	/**
932
	 * Convert and store an object request
933
	 * @param array $request  the request
934
	 * @param string $resultKey  if provided use this else create one
935
	 * @return string  return the resultKey
936
	 */
937 16
	private function createSqlAndStoreRequest(array $request, $resultKey = null)
938
	{
939
		// track number of calls to function to generate unique wthin one php request per call and per request result keys
940 16
		static $counter = 0;
941 16
		$counter++;
942
		// a randomly generated resultKey prevents db query chaching.
943 16
		$resultKey = $resultKey ? $resultKey : $request['requestKey'] . '_' . $counter;
944 16
		$request['resultKey'] = $resultKey;
945 16
		if (!array_key_exists($request['requestKey'], self::$_requestResultsCache))
946 16
			$this->convertRequestToSql($request);
947
		// store the request
948 10
		$this->objectRequests[$request['classType']][$resultKey] = $request;
949 10
		return $resultKey;
950
	}
951
952
	/**
953
	 * clear any object requests
954
	 */
955 10
	private function clearRequests()
956
	{
957 10
		$this->objectRequests = [];
958 10
		$this->boundValues = [];
959 10
	}
960
961
	/**
962
	 * clear all DdsObjectManager caches
963
	 */
964 70
	private function clearCaches()
965
	{
966 70
		self::$_requestResultsCache = [];
967 70
		self::$_linkResultsCache = [];
968 70
		self::$_mapResultsCache = [];
969 70
	}
970
971
	/**
972
	 * Extract all current requests off the queue and pass back
973
	 * @return array ['requests', 'boundValues']
974
	 */
975 6
	private function popRequests()
976
	{
977 6
		$currentRequests = $this->getObjectRequests();
978 6
		$this->clearRequests();
979 6
		return $currentRequests;
980
	}
981
982
	/**
983
	 * Replace all requests passed on the queue with those provided
984
	 * - use after popObjectRequests
985
	 * @param array $requests  an array of 'requests' and their
986
	 *   corresponding 'boundValues'
987
	 */
988 6
	private function pushRequests($requests)
989
	{
990 6
		$this->objectRequests = $requests['requests'];
991 6
		$this->boundValues = $requests['boundValues'];
992 6
	}
993
994
	/**
995
	 * convert a request into its SQL query for use on commit and store
996
	 * @param array $request
997
	 * @return string
998
	 */
999 16
	private function convertRequestToSql(&$request)
1000
	{
1001 16
		$includeDeleted = $request['deleted'];
1002 16
		$onlyDeleted = $this->onlyWantDeleted($includeDeleted, $request['filters']);
1003 16
		$classType = $request['classType'];
1004 16
		$table = $this->getTableFromClassType($classType);
1005 16
		$links = array_keys($this->getClassMembers($classType, ['link_multi', 'file_ref_multi']));
1006 16
		$where = $this->extractWhereClause($request['filters'], $includeDeleted, $links, $filterKeys);
1007 10
		$join  = $this->extractJoinClause($links, $filterKeys);
1008 10
		$order = $this->extractOrderClause($request['order']);
1009 10
		$limit = $this->extractLimitClause($request['limit']);
1010 10
		$request['sql'] = "SELECT DISTINCT o.*, t.*, '$request[requestKey]' as `{$this->requestKeyField}`, '$request[resultKey]' as `{$this->resultKeyField}` FROM $table t $join $where $order $limit";
1011
1012
		// try and find an efficient way of doing the really expensive count. This is a problem when
1013
		// we are trying to count a very large number of items and not filtering at all.
1014
		// Basically, if there are no filters, no links or we are only after deleted numbers
1015
		// then we can use the full class counts rather than counting on the queries directly.
1016 10
		$request['totalSql'] = null;
1017 10
		$useSimpleCount = ((empty($request['filters']) || $onlyDeleted) && empty($links));
1018 10
		if ($request['calculateTotal']) {
1019 2
			if ($useSimpleCount) {
1020
				if ($onlyDeleted) {
1021
					$request['totalSql'] = "SELECT `count_deleted` as `total`";
1022
				} else if ($includeDeleted) {
1023
					$request['totalSql'] = "SELECT `count_total` as `total`";
1024
				} else {
1025
					$request['totalSql'] = "SELECT (`count_total`-`count_deleted`) as `total`";
1026
				}
1027
				$request['totalSql'] .= ", '$request[requestKey]' as `{$this->requestKeyField}`, '$request[resultKey]_total' as `{$this->resultKeyField}` FROM `dds_class` WHERE `class_type` = '$classType'";
1028
			} else {
1029 2
				$request['totalSql'] =  "SELECT COUNT(*) as `total`, '$request[requestKey]' as `{$this->requestKeyField}`, '$request[resultKey]_total' as `{$this->resultKeyField}` FROM $table t $join $where";
1030
			}
1031
		}
1032 10
	}
1033
1034
	/**
1035
	 * See if only deleted objects are required
1036
	 * @return boolean
1037
	 */
1038 16
	private function onlyWantDeleted($includeDeleted, $filters)
1039
	{
1040 16
		return $includeDeleted && (count($filters) == 1) && isset($filters[0][0]) && $filters[0][0] = '_deleted' && isset($filters[0][2]) && $filters[0][2] == 1;
1041
	}
1042
1043
	/**
1044
	 * extract the join clause from the filters provided
1045
	 * @param array $links  the set of link fields in the table
1046
	 * @param array $filterKeys  the set of fields used in the filtering
1047
	 * @return string  the join clause
1048
	 */
1049 10
	private function extractJoinClause($links, $filterKeys)
1050
	{
1051
		// we're always joining the object table to the main table
1052 10
		$join = ['INNER JOIN dds_object `o` on `t`.`_uuid` = `o`.`_uuid`'];
1053
		// now see what else needs to be added
1054 10
		foreach ($links as $l) {
1055 4
			if (in_array($l, $filterKeys))
1056 4
				$join[] = "LEFT JOIN dds_link `link_$l` ON (`link_$l`.`from_id`=`o`.`_uuid` AND `link_$l`.`from_member`='$l')";
1057
		}
1058 10
		return implode(' ', $join);
1059
	}
1060
1061
	/**
1062
	 * extract the where clause from the filters provided
1063
	 * @param array $filters
1064
	 * @param boolean $includeDeleted  true if including deleted
1065
	 * @param array $links  any filter keys that are links
1066
	 * @param array &$filterKeys  the set of keys used as filters for use elsewhere
1067
	 * @return string
1068
	 */
1069 16
	private function extractWhereClause($filters, $includeDeleted, $links, &$filterKeys)
1070
	{
1071 16
		$filterKeys = [];
1072 16
		$notDeleted = '`o`.`_deleted`=0';
1073 16
		if (count($filters) == 0)
1074 6
			return $includeDeleted ? '' : "WHERE $notDeleted";
1075 14
		$this->createFilterClause($filters, $links, $filterKeys);
1076 8
		if (!$includeDeleted)
1077 8
			return "WHERE $notDeleted AND ($filters[sql])";
1078
		return "WHERE $filters[sql]";
1079
	}
1080
1081
	/**
1082
	 * recursively go through the filters and generate the sql filter
1083
	 * clause that's used for the where clause
1084
	 * @param array &$filters   the set of filters that are to be
1085
	 *   traversed to generate the overall where clause
1086
	 * @param array $links  the set of link members that aren't stored in this
1087
	 *   table
1088
	 * @param array &$filterKeys  the set of members that are being filtered on
1089
	 */
1090 14
	private function createFilterClause(&$filters, $links, &$filterKeys)
1091
	{
1092
		// filter down until we hit the actual filters
1093 14
		if (is_array($filters[0])) {
1094 14
			foreach ($filters as $k => &$filter) {
1095 14
				if ($k === 'logic')
1096 8
					continue;
1097 14
				$this->createFilterClause($filter, $links, $filterKeys);
1098
			}
1099
1100
			//
1101
			// to get here we're bubbling back up the recursion again
1102
			//
1103 14
			if (isset($filters[0]['itemSql'])) {
1104
				// ok, so now we should have itemSql's defined
1105
				// in which case we combine those with to get the filterSql
1106 14
				$filterSql = [];
1107 14
				foreach ($filters as $k => &$filter) {
1108 14
					if ($k === 'logic')
1109 8
						continue;
1110
					// use the logic keys if any for the items - these can be in position 3
1111
					// for most operators and position 2 for operators that don't take a key
1112 14
					if ($this->operatorTakesObject($filter[1])) {
1113 14
						if (isset($filter[3]))
1114 8
							$filterSql[$filter[3]] = $filter['itemSql'];
1115
						else
1116 14
							$filterSql[] = $filter['itemSql'];
1117
					} else {
1118 2
						if (isset($filter[2]))
1119 2
							$filterSql[$filter[2]] = $filter['itemSql'];
1120
						else
1121
							$filterSql[] = $filter['itemSql'];
1122
					}
1123
				}
1124 14
				if (empty($filters['logic']))
1125 6
					$filters['filterSql'] = $filters['sql'] = implode(' AND ', $filterSql);
1126
				else {
1127
					// make sure we test logic filters in order from longest key to shortest key
1128
					// otherwise we can end up with subkeys screwing things up
1129 8
					$orderedKeys = array_map('strlen', array_keys($filterSql));
1130 8
					array_multisort($orderedKeys, SORT_DESC, $filterSql);
0 ignored issues
show
Bug introduced by
neon\daedalus\services\ddsManager\SORT_DESC cannot be passed to array_multisort() as the parameter $rest expects a reference. ( Ignorable by Annotation )

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

1130
					array_multisort($orderedKeys, /** @scrutinizer ignore-type */ SORT_DESC, $filterSql);
Loading history...
1131 8
					$this->checkLogic(array_keys($filterSql), $filters['logic']);
1132
1133
					// make sure that keys that are subsets of variables or other keys
1134
					// don't interfere by converting all to their md5 hash and doing a
1135
					// double conversion,
1136 2
					$keys = array_keys($filterSql);
1137 2
					$keys2hashed = array();
1138 2
					foreach ($keys as $k)
1139 2
						$keys2hashed[$k] = md5($k);
1140 2
					$logicPass1 = str_replace($keys, $keys2hashed, $filters['logic']);
1141 8
					$filters['filterSql'] = $filters['sql'] = str_replace($keys2hashed, array_values($filterSql), $logicPass1);
1142
				}
1143
			} else {
1144
				// or we have filterSql's defined in which case combine these
1145
				// to get the complete set of filters
1146 8
				$clauseSql = [];
1147 8
				foreach ($filters as &$filter) {
1148 8
					$clauseSql[] = $filter['filterSql'];
1149 8
					unset($filter['sql']);
1150
				}
1151 8
				$filters['sql'] = '(' . implode(') OR (', $clauseSql) . ')';
1152
			}
1153 8
			return;
1154
		}
1155
		//
1156
		// To get here we're at the leaf of the recursion tree
1157
		// Start creating the sql. $filters[0,1] will have been canonicalised, $filters[2] is protected using PDO
1158
		// Not all queries have a [2] though so should be just the operator (e.g. IS NULL)
1159
		//
1160 14
		$filter = (in_array($filters[0], $links) ? "`link_{$filters[0]}`.`to_id`" : $this->quoteField($filters[0]));
1161 14
		$filterKeys[$filters[0]] = $filters[0];
1162 14
		if ($this->operatorTakesObject($filters[1]) && isset($filters[2]))
1163 14
			$filters['itemSql'] = $filters['sql'] = "$filter " . $this->prepareForPDO($filters[1], $filters[2]);
1164
		else {
1165 2
			$filters['itemSql'] = $filters['sql'] = "$filter $filters[1]";
1166
		}
1167 14
	}
1168
1169 14
	private function prepareForPDO($operator, $value)
1170
	{
1171 14
		if (is_array($value)) {
1172 6
			$pdoValues = [];
1173 6
			if (!in_array($operator, ['=', '!=', 'IN', 'NOT IN']))
1174
				throw new \InvalidArgumentException("Daedalus: Cannot pass an *array* of values with an operator of $operator");
1175 6
			foreach ($value as $v) {
1176 6
				if (is_array($v))
1177
					throw new \InvalidArgumentException("Daedalus: Cannot pass an *array* of *array* of values as filters (silly billy). You passed " . print_r($value, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($value, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

1177
					throw new \InvalidArgumentException("Daedalus: Cannot pass an *array* of *array* of values as filters (silly billy). You passed " . /** @scrutinizer ignore-type */ print_r($value, true));
Loading history...
1178 6
				$count = count($this->boundValues);
1179 6
				$variable = ":var{$count}end";
1180 6
				$this->boundValues[$variable] = $v;
1181 6
				$pdoValues[] = $variable;
1182
			}
1183
			// converts equals to ins and outs
1184 6
			if ($operator == '=')
1185 6
				$operator = 'IN';
1186 6
			if ($operator == '!=')
1187 6
				$operator = 'NOT IN';
1188 6
			return "$operator (" . implode(',', $pdoValues) . ')';
1189
		} else {
1190 10
			$count = count($this->boundValues);
1191 10
			$variable = ":var{$count}end";
1192 10
			if (strpos(strtolower($operator), 'like') !== false) {
1193 2
				$this->boundValues[$variable] = "%$value%";
1194 2
				return "$operator $variable";
1195 10
			} else if (strpos(strtolower($operator), 'null') !== false) {
1196
				return "$operator";
1197
			} else {
1198 10
				$this->boundValues[$variable] = $value;
1199 10
				return "$operator $variable";
1200
			}
1201
		}
1202
	}
1203
1204
	/**
1205
	 * Extract an order clause from the canonicalised order array
1206
	 * @param array $order
1207
	 * @return string
1208
	 */
1209 10
	private function extractOrderClause($order)
1210
	{
1211 10
		if (count($order) == 0)
1212 8
			return '';
1213 4
		$clause = [];
1214 4
		foreach ($order as $o => $d) {
1215
			// if any of the order clauses are RAND then replace whole clause
1216 4
			if ($o === 'RAND') {
1217
				$clause = ["RAND()"];
1218
				break;
1219
			}
1220
			// allow negative ordering in mysql for nulls last
1221 4
			if (strpos($o, '-') === 0)
1222 2
				$clause[] = '-' . substr($o, 1) . " $d";
1223
			else
1224 2
				$clause[] = "$o $d";
1225
		}
1226 4
		return 'ORDER BY ' . implode(', ', $clause);
1227
	}
1228
1229
	/**
1230
	 * Extract a limit clause from the canonicalised limit array
1231
	 * @param array $limit
1232
	 * @return string
1233
	 */
1234 10
	private function extractLimitClause($limit)
1235
	{
1236 10
		if (count($limit) == 0)
1237
			return '';
1238 10
		return "LIMIT $limit[start], $limit[length]";
1239
	}
1240
1241
	/**
1242
	 * Set member links in bulk into the links table
1243
	 * @param array $objectsLinks  an array of $fromUuid => $toMemberLinks
1244
	 *   where uuid is the from uuid and $toMemberLinks is an array of
1245
	 */
1246 70
	private function setMembersLinks($objectsLinks, $chunkSize)
1247
	{
1248 70
		if (count($objectsLinks) === 0)
1249
			return 0;
1250 70
		$totalCount = 0;
1251 70
		$db = neon()->db;
1252 70
		$ddsLink = DdsLink::tableName();
1253 70
		foreach (array_chunk($objectsLinks, $chunkSize, true) as $chunkData) {
1254 70
			$count = 0;
1255 70
			$batchInsert = [];
1256 70
			$deleteLinkClauses = [];
1257 70
			foreach ($chunkData as $fromUuid => $toMemberLinks) {
1258
				// check you've been supplied with rinky dinky data
1259 70
				if (!$this->isUUID($fromUuid))
1260
					throw new \InvalidArgumentException("The fromUuid should be a UUID64. You passed in $fromUuid");
1261 70
				foreach ($toMemberLinks as $member => $toLinks) {
1262 8
					if (!is_array($toLinks))
1263
						$toLinks = [$toLinks];
1264
					// remove any empty toLinks that might have snuck through
1265 8
					$toLinks = array_filter($toLinks);
1266 8
					if (!$this->areUUIDs($toLinks))
1267
						throw new \InvalidArgumentException("The toMemberLinks should be UUID64s. You passed in " . print_r($toLinks, true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($toLinks, true) of type string|true can be used in concatenation? ( Ignorable by Annotation )

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

1267
						throw new \InvalidArgumentException("The toMemberLinks should be UUID64s. You passed in " . /** @scrutinizer ignore-type */ print_r($toLinks, true));
Loading history...
1268 8
					$toLinks = array_unique($toLinks);
1269
1270
					//
1271
					// set up any existing links from from_id:member to be deleted
1272
					//
1273
					// There is an ambiguity here with what to do about soft-deleted objects that
1274
					// are linked. You could leave these here, but then if the other object
1275
					// was undeleted, then would you expect or be surprised if those links reappeared?
1276
					// I don't think there is any correct policy here so choosing to delete the links
1277
					//
1278 8
					$deleteLinkClauses[] = "(`from_id`='$fromUuid' AND `from_member`='$member')";
1279
1280
					// prepare the new links ready for bulk insert
1281 8
					$count += count($toLinks);
1282 8
					foreach ($toLinks as $toLink)
1283 8
						$batchInsert[] = ['from_id' => $fromUuid, 'from_member' => $member, 'to_id' => $toLink];
1284
				}
1285
			}
1286 70
			if (count($deleteLinkClauses)) {
1287 8
				$deleteClause = implode(' OR ', $deleteLinkClauses);
1288 8
				$db->createCommand()->delete($ddsLink, $deleteClause)->execute();
1289
			}
1290
1291 70
			$insertedCount = 0;
1292 70
			if (count($batchInsert))
1293 8
				$insertedCount = $db->createCommand()->batchInsert($ddsLink, ['from_id', 'from_member', 'to_id'], $batchInsert)->execute();
1294 70
			if (YII_DEBUG && $insertedCount !== $count)
1295
				throw new \Exception("The link insertion failed - $count items should have been inserted vs $insertedCount actual");
1296 70
			$totalCount += $count;
1297
		}
1298 70
		return $totalCount;
1299
	}
1300
1301
	/**
1302
	 * Insert a series of links from object a to set of objects b
1303
	 *
1304
	 * @param string $fromUuid - a UUID64
1305
	 * @param [member_ref][string] $toMemberLinks - an array of UUID64s for each
0 ignored issues
show
Documentation Bug introduced by
The doc comment [member_ref][string] at position 0 could not be parsed: Unknown type name '[' at position 0 in [member_ref][string].
Loading history...
1306
	 *   member_ref keys are set for (so we can distinguish between not set for
1307
	 *   updates (=> do nothing) and set but blank (=> delete)
1308
	 * @return integer - the number of inserted links
1309
	 */
1310 66
	private function setMemberLinks($fromUuid, array $toMemberLinks)
1311
	{
1312 66
		return $this->setMembersLinks([$fromUuid=>$toMemberLinks], 1);
1313
	}
1314
1315
	/**
1316
	 * Get all member links associated with this object
1317
	 *
1318
	 * @param uuid $uuid
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsManager\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...
1319
	 * @return array  an array of all object ids linked against
1320
	 *   a member of this object e.g. ['author_id']=>[ blogIds ]
1321
	 */
1322 28
	private function getMemberLinks($uuid)
1323
	{
1324 28
		$membersLinks = $this->getMembersLinks([$uuid]);
1325 28
		return $membersLinks[$uuid];
1326
	}
1327
1328
	/**
1329
	 * Get the membersLinks associated with particular fields on objects
1330
	 * @param array $objectIds  - the set of objects you want the fields for
1331
	 * @return array  an array of [objectId]=>[member_ref=>links]. All objects
1332
	 *   passed in have an array returned even if that is an empty array
1333
	 */
1334 40
	protected function getMembersLinks(array $objectIds)
1335
	{
1336 40
		if (empty($objectIds))
1337 18
			return [];
1338 36
		$objectIds = array_unique($objectIds);
1339 36
		sort($objectIds, SORT_STRING);
1340 36
		$cacheKey = md5(serialize($objectIds));
1341 36
		if (!array_key_exists($cacheKey, self::$_linkResultsCache)) {
1342 36
			$rows = DdsLink::find()->noCache()->select(['from_id', 'from_member', 'to_id', '_deleted'])
1343 36
				->join('LEFT JOIN', '{{%dds_object}}', 'to_id=_uuid')
1344 36
				->where('`_deleted` IS NULL OR `_deleted` = 0')
1345 36
				->andWhere(['from_id' => $objectIds])
1346 36
				->asArray()
1347 36
				->all();
1348 36
			$multilinks = array_fill_keys($objectIds, []);
1349 36
			foreach ($rows as $row)
1350 6
				$multilinks[$row['from_id']][$row['from_member']][] = $row['to_id'];
1351 36
			self::$_linkResultsCache[$cacheKey] = $multilinks;
1352
		}
1353 36
		return self::$_linkResultsCache[$cacheKey];
1354
	}
1355
1356
	/**
1357
	 * Send a change log entry to the change log manager
1358
	 * @param DdsClass $class  the DdsClass Object
1359
	 * @param string $changeKey  the change key for the action performed e.g. EDIT
1360
	 * @param string $objectUuid  the object uuid
1361
	 * @param array $changes  what the changes are
1362
	 * @return string  the uuid of the log entry
1363
	 */
1364 10
	private function createChangeLogEntry($class, $changeKey, $objectUuid, $originalValues = [], $newValues = [])
1365
	{
1366 10
		static $changeLog = null;
1367 10
		if (!$changeLog)
1368 2
			$changeLog = neon('dds')->IDdsChangeLogManagement;
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsChangeLogManagement does not exist on neon\core\ApplicationWeb. Since you implemented __get, consider adding a @property annotation.
Loading history...
1369 10
		return $changeLog->addLogEntry($objectUuid, $class->toArray(), $changeKey, $originalValues, $newValues);
1370
	}
1371
1372
	/**
1373
	 * Check that the uuids provided are all of the given class type.
1374
	 * Remove any that aren't.
1375
	 * @param array $uuids  the set of uuids
1376
	 * @param string $classType  the class type they should all belong to
1377
	 * @return array  the set of uuids that all belong to this class type
1378
	 */
1379 4
	private function checkUuidsAgainstClassType($uuids, $classType)
1380
	{
1381
		// remove any objects that aren't of the same type as the class type
1382 4
		$db = neon()->db;
1383 4
		$uuidClause = [];
1384 4
		$objectValues = [];
1385 4
		$count = 0;
1386 4
		foreach ($uuids as $id) {
1387 4
			$placeHolder = ":v$count";
1388 4
			$uuidClause[] = $placeHolder;
1389 4
			$objectValues[$placeHolder] = $id;
1390 4
			$count++;
1391
		}
1392 4
		$uuidClause = '(' . implode(',', $uuidClause) . ')';
1393 4
		$query = "SELECT [[_uuid]] FROM [[dds_object]] WHERE [[_class_type]]='$classType' AND [[_uuid]] IN $uuidClause";
1394 4
		$checkedUuidResults = $db->createCommand($query)->bindValues($objectValues)->queryAll();
1395 4
		return array_column($checkedUuidResults, '_uuid');
1396
	}
1397
}
1398