Passed
Push — develop ( 5f0710...340c0b )
by Neill
12:23 queued 14s
created

DdsObjectManager   F

Complexity

Total Complexity 205

Size/Duplication

Total Lines 1296
Duplicated Lines 0 %

Test Coverage

Coverage 93.91%

Importance

Changes 6
Bugs 0 Features 0
Metric Value
wmc 205
eloc 616
dl 0
loc 1296
rs 1.984
c 6
b 0
f 0
ccs 601
cts 640
cp 0.9391

45 Methods

Rating   Name   Duplication   Size   Complexity  
A getObjectMap() 0 3 1
B destroyObjects() 0 52 10
A getObjectFromId() 0 4 1
A runSingleObjectRequest() 0 16 2
A pushRequests() 0 4 1
A convertRequestToSql() 0 12 2
B listObjects() 0 46 11
A getLastCommitData() 0 3 2
B setMemberLinks() 0 36 9
A addObjectRequest() 0 22 1
A getObjectMapManager() 0 5 2
A createChangeLogEntry() 0 6 2
A checkUuidsAgainstClassType() 0 17 2
A clearRequests() 0 4 1
D addObject() 0 68 12
A makeMapLookupRequest() 0 3 1
B deleteObjects() 0 36 10
A extractJoinClause() 0 8 3
B destroyObject() 0 36 8
A getMemberLinks() 0 4 1
A popRequests() 0 5 1
A hasObjectRequests() 0 3 1
A undeleteObject() 0 21 6
B prepareForPDO() 0 31 9
A getMembersLinks() 0 20 4
A createSqlAndStoreRequest() 0 12 3
A extractLimitClause() 0 5 2
A getObjectRowsFromIds() 0 5 1
A makeMapChainLookupRequest() 0 3 1
D editObjects() 0 80 18
A extractOrderClause() 0 18 5
B addObjects() 0 53 9
C editObject() 0 70 12
B commitRequests() 0 75 11
A getObjectRequests() 0 5 1
A extractWhereClause() 0 10 4
A getMapLookupResults() 0 3 1
A convertResultsToPHP() 0 14 5
A clearMapRequests() 0 3 1
A getMapChainLookupResults() 0 3 1
A clearCaches() 0 5 1
A deleteObject() 0 29 6
A getObject() 0 12 3
A commitMapRequests() 0 3 1
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
 * @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\ddsManager;
9
10
use neon\daedalus\events\ObjectEditEvent;
11
use neon\daedalus\events\ObjectAddEvent;
12
use neon\daedalus\interfaces\IDdsObjectManagement;
13
use neon\daedalus\services\ddsManager\models\DdsObject;
14
use neon\daedalus\services\ddsManager\models\DdsLink;
15
16
class DdsObjectManager
17
extends DdsCore
18
implements IDdsObjectManagement
19
{
20
21
	/** ---------------------------------------------------- */
22
	/** ---------- interface IDdsObjectManagement ---------- */
23
	/** ---------------------------------------------------- */
24
25
	/**
26
	 * Caches for results from queries so can protect against over-zealous
27
	 * requests to the database within one call
28
	 * @var array
29
	 */
30
	private static $_requestResultsCache=[];
31
	private static $_linkResultsCache=[];
32
	private static $_mapResultsCache=[];
33
34
	/**
35
	 * @inheritdoc
36
	 */
37 16
	public function addObjectRequest($classType, array $filters=[], array $order=[], array $limit=[], $resultKey=null, $includeDeleted=false)
38
	{
39
		// protect ourselves against SQL injection by making sure all
40
		// items are canonicalised as required (and use of PDO later)
41 16
		$classType = $this->canonicaliseRef($classType);
42 16
		$filters = $this->canonicaliseFilters($filters);
43 16
		$order = $this->canonicaliseOrder($order);
44 16
		$limit = $this->canonicaliseLimit($limit, $total, $calculateTotal);
45
46
		$request = [
47 16
			'classType' => $classType,
48 16
			'filters' => $filters,
49 16
			'order' => $order,
50 16
			'limit' => $limit,
51 16
			'total' => $total,
52 16
			'calculateTotal' => $calculateTotal,
53 16
			'deleted' => $includeDeleted
54
		];
55
		// add a request key so we can cache request results more easily
56 16
		$request['requestKey'] = md5(serialize($request));
57 16
		$result = $this->createSqlAndStoreRequest($request, $resultKey);
58 10
		return $result;
59
	}
60
61
	/**
62
	 * @inheritdoc
63
	 */
64 6
	public function getObjectRequests()
65
	{
66
		return [
67 6
			'requests' => $this->objectRequests,
68 6
			'boundValues' => $this->boundValues
69
		];
70
	}
71
72
	/**
73
	 * @inheritdoc
74
	 */
75 10
	public function hasObjectRequests()
76
	{
77 10
		return !empty($this->objectRequests);
78
	}
79
80
	/**
81
	 * @inheritdoc
82
	 */
83 10
	public function commitRequests()
84
	{
85
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 16
	public function listObjects($classType, &$total=null, $includeDeleted=false, $inFull=true, $start=0, $length=100, $order=null)
255
	{
256 16
		$classType = $this->canonicaliseRef($classType);
257 16
		if (empty($order))
258 16
			$order = ['_created'=>'DESC'];
259 16
		$order = $this->canonicaliseOrder($order);
260
261
		// get the total first
262 16
		$class = null;
263 16
		$this->findClass($classType, $class, true);
264 16
		$total = $class->count_total;
265 16
		if (!$includeDeleted)
266 16
			$total -= $class->count_deleted;
267
268
		// now get the objects
269 16
		$query = (new \neon\core\db\Query)
270 16
			->from('{{dds_object}} o')
271 16
			->select($inFull ? "[[t]].*, [[o]].*" : "[[o]].*")
272 16
			->where(['[[o]].[[_class_type]]'=>$classType])
273 16
			->offset($start)
274 16
			->limit($length);
275
276 16
		if (!$includeDeleted)
277 16
			$query->andWhere('[[o]].[[_deleted]] = 0');
278 16
		if ($inFull) {
279 16
			$tableName = $this->getTableFromClassType($classType);
280 16
			$query->innerJoin('{{'.$tableName.'}} t', '[[o]].[[_uuid]]=[[t]].[[_uuid]]');
281
		}
282
283 16
		if (is_array($order)) {
0 ignored issues
show
introduced by
The condition is_array($order) is always false.
Loading history...
284 16
			$orderBits = [];
285 16
			foreach ($order as $k=>$d) {
286 16
				if ($k[1]=='o') {
287 16
					$orderBits[] = "$k $d";
288 2
				} else if ($inFull) {
289 2
					$orderBits[] = "[[t]].$k $d";
290
				}
291
			}
292 16
			if ($orderBits)
293 16
				$orderClause = implode(', ',$orderBits);
294 16
			$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 16
		$data = $query->all();
298 16
		$this->convertResultsToPHP($data);
299 16
		return $data;
300
	}
301
302
	/**
303
	 * @inheritdoc
304
	 */
305 64
	public function addObject($classType, array $data, &$changeLogUuid=null)
306
	{
307 64
		$class = null;
308 64
		$classType = $this->canonicaliseRef($classType);
309 64
		$this->findClass($classType, $class, true);
310 64
		if ($class->hasChangeLog())
311 8
			$newValues = $data;
312 64
		$table = $this->getTableFromClassType($classType);
313
314
		// create the new object
315 64
		$object = new DdsObject;
316 64
		$now = date('Y-m-d H:i:s');
317 64
		$uuid = (!empty($data['_uuid']) && $this->isUUID($data['_uuid'])) ? $data['_uuid'] : $this->generateUUID();
318 64
		$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 64
		$object->_class_type = $classType;
320 64
		$object->_created = $now;
321 64
		$object->_updated = $now;
322 64
		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 64
		$class->count_total += 1;
327 64
		$class->save();
328
329
		// then insert all of the associated data
330
		try {
331 64
			$this->convertFromPHPToDB($classType, $data, $links);
332 64
			$memberStorage = $this->getMemberStorage($classType);
333 64
			$count = 0;
334 64
			$fields = [];
335 64
			$inserts = [];
336 64
			$values = [];
337 64
			foreach (array_keys($memberStorage) as $memRef) {
338
				// save all non-empty fields.
339 64
				if (array_key_exists($memRef, $data) && $data[$memRef]!==null && $data[$memRef]!=='') {
340 58
					$fields[] = "`$memRef`";
341 58
					$inserts[] = ":v$count";
342 58
					$values[":v$count"] = $data[$memRef];
343 58
					$count++;
344
				}
345
			}
346
347 64
			if (count($fields))
348 58
				$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 64
			$command = neon()->db->createCommand($query);
352 64
			$command->bindValues($values);
353 64
			$command->execute();
354
355
			// if there are any links then add these to the link table
356 64
			$this->setMemberLinks($object->_uuid, $links);
357
358 64
			if ($class->hasChangeLog())
359 64
				$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 64
		$this->clearCaches();
367
368 64
		$this->trigger("dds.afterAdd.$classType", new ObjectAddEvent([
369 64
			'classType' => $classType,
370 64
			'object' => array_merge($data, $object->toArray())
371
		]));
372 64
		return $object->_uuid;
373
	}
374
375
	/**
376
	 * TODO: make this function better - currently used for testing -
377
	 * has duplicate code and doesn't add links in efficiently
378
	 *
379
	 * @inheritdoc
380
	 */
381 2
	public function addObjects($classType, array $data, $chunkSize=1000)
382
	{
383 2
		$classType = $this->canonicaliseRef($classType);
384 2
		$class = null;
385 2
		$this->findClass($classType, $class, true);
386 2
		$table = $this->getTableFromClassType($classType);
387
388
		// get a list of the columns
389 2
		$columns = [];
390 2
		$nullRow = [];
391 2
		$memberStorage = $this->getMemberStorage($classType);
392 2
		foreach (array_keys($memberStorage) as $memRef) {
393 2
			$columns[] = $memRef;
394 2
			$nullRow[$memRef] = null;
395
		}
396 2
		$columns[] = '_uuid';
397 2
		$nullRow['_uuid'] = null;
398
399 2
		foreach(array_chunk($data, $chunkSize) as $chunkData) {
400 2
			$rows = [];
401 2
			$objectDbRows = [];
402 2
			$objectsLinks = [];
403 2
			$now = date('Y-m-d H:i:s');
404 2
			foreach ($chunkData as $row) {
405 2
				if ($class->hasChangeLog())
406 2
					$newValues = $row;
407
				// ddt table value - format row data and append the uuid
408 2
				$this->convertFromPHPToDB($classType, $row, $links);
409 2
				$uuid = (!empty($row['_uuid']) && $this->isUUID($row['_uuid'])) ? $row['_uuid'] : $this->generateUUID();
410 2
				$row['_uuid'] = $uuid;
411
				// only include data that belongs in the table and null value remainder
412 2
				$rows[] = array_replace($nullRow, array_intersect_key($row, $nullRow));
413
				// object table row value
414 2
				$objectDbRows[] = [$row['_uuid'], $classType, $now, $now];
415 2
				$objectsLinks[$row['_uuid']] = $links;
416 2
				if ($class->hasChangeLog())
417 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

417
					$this->createChangeLogEntry($class, 'ADD', /** @scrutinizer ignore-type */ $uuid, [], $newValues);
Loading history...
418
			}
419 2
			neon()->db->createCommand()->batchInsert(DdsObject::tableName(), ['_uuid', '_class_type', '_created', '_updated'], $objectDbRows)->execute();
420 2
			neon()->db->createCommand()->batchInsert($table, $columns, $rows)->execute();
421
422 2
			foreach($objectsLinks as $uuid => $links) {
423
				// if there are any links then add these to the link table
424
				//
425
				// TODO - make this a bulk insert across all data
426
				//
427 2
				$this->setMemberLinks($uuid, $links);
428
			}
429
430 2
			$class->count_total += count($chunkData);
431 2
			$class->save();
432
		}
433 2
		$this->clearCaches();
434 2
	}
435
436
	/**
437
	 * @inheritdoc
438
	 */
439 28
	public function getObject($uuid)
440
	{
441 28
		$object = null;
442 28
		if ($this->getObjectFromId($uuid, $object)) {
443 28
			$table = $this->getTableFromClassType($object->_class_type);
444 28
			$query = "SELECT `o`.*, `t`.* FROM `dds_object` `o` LEFT JOIN  `$table` `t` ON `o`.`_uuid`=`t`.`_uuid` WHERE `o`.`_uuid`=:uuid LIMIT 1";
445 28
			$rows = neon()->db->createCommand($query)->bindValue(':uuid', $uuid)->queryAll();
446 28
			$row =  count($rows) > 0 ? $rows[0] : null;
447 28
			$this->convertFromDBToPHP($row, $this->getMemberLinks($uuid));
448 28
			return $row;
449
		}
450 8
		return null;
451
	}
452
453
	/**
454
	 * @inheritdoc
455
	 */
456 20
	public function editObject($uuid, array $changes, &$changeLogUuid=null)
457
	{
458
		// check there are some changes to make
459 20
		if (count($changes)==0)
460
			return false;
461
462
		// check that the object exists and find its class type
463 20
		if (!$this->getObjectFromId($uuid, $object))
464
			throw new \InvalidArgumentException("Couldn't edit object with id $uuid as not found");
465
466
		// see if we need to store the change log
467 20
		$class = null;
468 20
		$this->findClass($object['_class_type'], $class);
469
470 20
		$object->_updated = date('Y-m-d H:i:s');
471 20
		if (!$object->save())
472
			return $object->errors;
473
474
		// now update any member changes
475
		try {
476
			// note before and after values if has a change log
477 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...
478 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

478
			$memberStorage = array_diff_key(array_keys(/** @scrutinizer ignore-type */ $currentObject), array_keys($object->attributes));
Loading history...
479 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

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

552
					$beforeValues[$uuid] = array_intersect_key(/** @scrutinizer ignore-type */ $this->getObject($uuid), $changes);
Loading history...
553 2
				$newValues = $changes;
554
			}
555
556
			// make the changes
557 4
			$this->convertFromPHPToDB($classType, $changes, $links);
558 4
			$count = 0;
559 4
			$updates = [];
560 4
			$values = [];
561 4
			foreach (array_keys($memberStorage) as $memRef) {
562 4
				if (array_key_exists($memRef, $changes)) {
563
					// when saving changes, need to manage empty fields by setting to null
564
					// otherwise saving what has been provided
565 4
					if ($changes[$memRef] === null || $changes[$memRef] === '') {
566
						$updates[] = "`$memRef`=NULL";
567
					} else {
568 4
						$updates[] = "`$memRef`=:v$count";
569 4
						$values[":v$count"] = $changes[$memRef];
570 4
						$count++;
571
					}
572
				}
573
			}
574 4
			if (count($updates)==0)
575 2
				return false;
576
577
			// sort out the uuid clause for both the update on the table and in the object table
578 4
			$uuidClause = [];
579 4
			$objectValues = [];
580 4
			foreach ($uuids as $id) {
581 4
				$placeHolder = ":v$count";
582 4
				$uuidClause[] = $placeHolder;
583 4
				$values[$placeHolder] = $id;
584 4
				$objectValues[$placeHolder] = $id;
585 4
				$count++;
586
			}
587 4
			$uuidClause = '('.implode(',', $uuidClause).')';
588
589
			// update the latest changed
590 4
			$db = neon()->db;
591 4
			$query = "UPDATE `dds_object` SET `_updated`=NOW() WHERE `_uuid` IN $uuidClause LIMIT ".count($uuids);
592 4
			$db->createCommand($query)->bindValues($objectValues)->execute();
593
			// make the changes
594 4
			$query = "UPDATE `$table` SET ".implode(', ',$updates)." WHERE `_uuid` IN $uuidClause LIMIT ".count($uuids);
595 4
			$db->createCommand($query)->bindValues($values)->execute();
596
597
			//
598
			// do NOT update any member links - that would likely be a programmatic error
599
			//
600
601
			// and save entries into a change log
602 4
			if ($class && $class->hasChangeLog()) {
603 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...
604 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...
605
			}
606
607
		} catch (\yii\db\exception $e) {
608
			$this->clearCaches();
609
			return ['error'=>$e->getMessage()];
610
		}
611 4
		$this->clearCaches();
612 4
		return true;
613
	}
614
615
	/**
616
	 * @inheritdoc
617
	 */
618 40
	public function deleteObject($uuid, &$changeLogUuid=null)
619
	{
620 40
		$object = null;
621
		// find the object and delete if it hasn't already been
622 40
		if ($this->getObjectFromId($uuid, $object) && $object->_deleted==0) {
623 40
			$object->_deleted = 1;
624 40
			$object->_updated = date('Y-m-d H:i:s');
625 40
			if (!$object->save())
626
				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

626
				throw new \RuntimeException("Couldn't delete the object: "./** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
627
			// and update the class object deleted count
628 40
			$class = null;
629 40
			$this->findClass($object['_class_type'], $class);
630 40
			$class->count_deleted += 1;
631 40
			$class->save();
632
633
			// see if we need to store the change log
634 40
			if ($class && $class->hasChangeLog())
635 8
				$changeLogUuid = $this->createChangeLogEntry($class, 'DELETE', $uuid);
636
637
			// ---- KEEP ME ----
638
			// on't delete the links so any that haven't been deleted between
639
			// deletion and undeletion can be recreated. The most likely usecase
640
			// here is an accidental delete followed by an immediate undelete.
641
			// As far as I can see there is no definitive solution here that covers
642
			// all possible usecases, so this is a behavioural choice.
643
			// See setMemberLinks for more on this choice.
644
			// ---- KEEP ME ----
645
646 40
			$this->clearCaches();
647
		}
648 40
	}
649
650
	/**
651
	 * @inheritdoc
652
	 */
653 4
	public function deleteObjects(array $uuids)
654
	{
655 4
		$objects = $this->getObjectRowsFromIds($uuids);
656 4
		if (count($objects)==0)
657
			return;
658
659 4
		$foundUuids = [];
660 4
		$foundClasses = [];
661 4
		$deletedUuids = [];
662 4
		foreach ($objects as $obj) {
663 4
			if ($obj['_deleted'] == 0) {
664 4
				$foundUuids[$obj['_uuid']] = $obj['_uuid'];
665 4
				$classType = $obj['_class_type'];
666 4
				if (!isset($foundClasses[$classType])) {
667 4
					$foundClasses[$classType] = 1;
668
				} else {
669 2
					$foundClasses[$classType] += 1;
670
				}
671 4
				$deletedUuids[$classType][$obj['_uuid']] = $obj['_uuid'];
672
			}
673
		}
674 4
		if (count($foundUuids))
675 4
			DdsObject::updateAll(['_deleted'=>1, '_updated'=>date('Y-m-d H:i:s')], ['_uuid'=>$foundUuids]);
676 4
		foreach ($foundClasses as $type=>$count) {
677 4
			$class = null;
678 4
			if ($this->findClass($type, $class)) {
679 4
				$class->count_deleted += $count;
680 4
				$class->save();
681
				// TODO 20200107 Make a bulk call for change log entries
682 4
				if ($class->hasChangeLog()) {
683 2
					foreach ($deletedUuids[$class->class_type] as $objUuid)
684 2
						$this->createChangeLogEntry($class, 'DELETE', $objUuid);
685
				}
686
			}
687
		}
688 4
		$this->clearCaches();
689 4
	}
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 4
	public function destroyObjects(array $uuids)
764
	{
765 4
		$objects = $this->getObjectRowsFromIds($uuids);
766 4
		if (count($objects)==0)
767
			return;
768
769 4
		$objectUuids = [];
770 4
		$objectsDeleted = [];
771 4
		$classUuids = [];
772 4
		foreach ($objects as $obj) {
773 4
			$objectUuids[$obj['_uuid']] = $obj['_uuid'];
774 4
			if ($obj['_deleted'] == 1)
775 4
				$objectsDeleted[$obj['_class_type']][$obj['_uuid']] = $obj['_uuid'];
776 4
			$classUuids[$obj['_class_type']][] = $obj['_uuid'];
777
		}
778
779
		// delete all of the objects from their appropriate class table
780 4
		foreach ($classUuids as $classType => $uuids) {
781 4
			$class = null;
782 4
			$this->findClass($classType, $class);
783 4
			$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 4
			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 4
			neon()->db->createCommand()
796 4
				->delete($table, ['_uuid'=>$uuids])
797 4
				->execute();
798
799
			// decrement the class's object counts
800 4
			if ($class) {
801 4
				$class->count_total = max(0, $class->count_total-count($uuids));
802 4
				if (isset($objectsDeleted[$classType]))
803 4
					$class->count_deleted = max(0, $class->count_deleted-count($objectsDeleted[$classType]));
804 4
				$class->save();
805
			}
806
		}
807
808
		// delete all of the objects from the DdsObject table
809 4
		DdsObject::deleteAll(['_uuid'=>$objectUuids]);
810
811
		// finally, delete any links from or to the objects
812 4
		DdsLink::deleteAll(['or', ['from_id'=>$objectUuids], ['to_id'=>$objectUuids]]);
813
814 4
		$this->clearCaches();
815 4
	}
816
817
818
	/** -------------------------------------- **/
819
	/** ---------- Internal Methods ---------- **/
820
821
	/**
822
	 * @var \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
823
	 */
824
	private $_objectMapManager = null;
825
826
	/**
827
	 * @return \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
828
	 */
829 28
	private function getObjectMapManager()
830
	{
831 28
		if (!$this->_objectMapManager)
832 28
			$this->_objectMapManager = new DdsObjectMapManager;
833 28
		return $this->_objectMapManager;
834
	}
835
836
	/**
837
	 * convert a row of data from the database to php
838
	 * @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...
839
	 * @param string $classTypeKey the key in the data that will give the class type
840
	 */
841 20
	private function convertResultsToPHP(&$dataSet, $links=null, $classTypeKey='_class_type')
842
	{
843
		// if we haven't been provided any multilinks (even if empty array)
844
		// then see if there are any multilinks to extract
845 20
		if ($links === null) {
846 16
			$objectIds = [];
847 16
			foreach ($dataSet as $row)
848 14
				$objectIds[] = $row['_uuid'];
849 16
			$links = $this->getMembersLinks($objectIds);
850
		}
851 20
		foreach ($dataSet as &$data) {
852
			// convert full rows to database. Totals are left untouched
853 18
			if (isset($data[$classTypeKey]))
854 18
				$this->convertFromDBToPHP($data, $links[$data['_uuid']], $classTypeKey);
855
		}
856 20
	}
857
858
	/**
859
	 * Get the model object from its id
860
	 * @param string $uuid  the object id
861
	 * @param DdsObject &$object  the returned object
862
	 * @return boolean  whether or not object found
863
	 */
864 50
	private function getObjectFromId($uuid, &$object=null)
865
	{
866 50
		$object = DdsObject::find()->noCache()->where(['_uuid' => $uuid])->one();
867 50
		return ($object !== null);
868
	}
869
870
	/**
871
	 * Get the array data for a row from its id
872
	 * @param array $uuids  an array of object uuids
873
	 * @return array  the objects found
874
	 */
875 6
	private function getObjectRowsFromIds(array $uuids)
876
	{
877 6
		$uuids = array_unique($uuids);
878 6
		$rows = DdsObject::find()->noCache()->where(['_uuid' => $uuids])->asArray()->all();
879 6
		return $rows;
880
	}
881
882
	/**
883
	 * @var array  values bound for the next commit
884
	 */
885
	private $boundValues = [];
886
887
	/**
888
	 * @var string  internal name for request keys
889
	 */
890
	private $requestKeyField = "___request_key";
891
892
	/**
893
	 * @var string  internal name for result keys
894
	 */
895
	private $resultKeyField = "___result_key";
896
897
	/**
898
	 * @var array  store of the last commit
899
	 */
900
	private $lastCommitResults = [];
901
902
	/**
903
	 * @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...
904
	 */
905
	private $objectRequests = [];
906
907
	/**
908
	 * Convert and store an object request
909
	 * @param array $request  the request
910
	 * @param string $resultKey  if provided use this else create one
911
	 * @return string  return the resultKey
912
	 */
913 16
	private function createSqlAndStoreRequest(array $request, $resultKey=null)
914
	{
915
		// track number of calls to function to generate unique wthin one php request per call and per request result keys
916 16
		static $counter = 0; $counter ++;
917
		// a randomly generated resultKey prevents db query chaching.
918 16
		$resultKey = $resultKey ? $resultKey : $request['requestKey'].'_'.$counter;
919 16
		$request['resultKey' ] = $resultKey;
920 16
		if (!array_key_exists($request['requestKey'], self::$_requestResultsCache))
921 16
			$this->convertRequestToSql($request);
922
		// store the request
923 10
		$this->objectRequests[$request['classType']][$resultKey] = $request;
924 10
		return $resultKey;
925
	}
926
927
	/**
928
	 * clear any object requests
929
	 */
930 10
	private function clearRequests()
931
	{
932 10
		$this->objectRequests = [];
933 10
		$this->boundValues = [];
934 10
	}
935
936
	/**
937
	 * clear all DdsObjectManager caches
938
	 */
939 66
	private function clearCaches()
940
	{
941 66
		self::$_requestResultsCache = [];
942 66
		self::$_linkResultsCache = [];
943 66
		self::$_mapResultsCache = [];
944 66
	}
945
946
	/**
947
	 * Extract all current requests off the queue and pass back
948
	 * @return array ['requests', 'boundValues']
949
	 */
950 6
	private function popRequests()
951
	{
952 6
		$currentRequests = $this->getObjectRequests();
953 6
		$this->clearRequests();
954 6
		return $currentRequests;
955
	}
956
957
	/**
958
	 * Replace all requests passed on the queue with those provided
959
	 * - use after popObjectRequests
960
	 * @param array $requests  an array of 'requests' and their
961
	 *   corresponding 'boundValues'
962
	 */
963 6
	private function pushRequests($requests)
964
	{
965 6
		$this->objectRequests = $requests['requests'];
966 6
		$this->boundValues = $requests['boundValues'];
967 6
	}
968
969
	/**
970
	 * convert a request into its SQL query for use on commit and store
971
	 * @param array $request
972
	 * @return string
973
	 */
974 16
	private function convertRequestToSql(&$request)
975
	{
976 16
		$table = $this->getTableFromClassType($request['classType']);
977 16
		$links = array_keys($this->getClassMembers($request['classType'], ['link_multi','file_ref_multi']));
978 16
		$where = $this->extractWhereClause($request['filters'], $request['deleted'], $links, $filterKeys);
979 10
		$join  = $this->extractJoinClause($links, $filterKeys);
980 10
		$order = $this->extractOrderClause($request['order']);
981 10
		$limit = $this->extractLimitClause($request['limit']);
982 10
		$request['sql'] = "SELECT DISTINCT o.*, t.*, '$request[requestKey]' as `{$this->requestKeyField}`, '$request[resultKey]' as `{$this->resultKeyField}` FROM $table t $join $where $order $limit";
983 10
		$request['totalSql'] = $request['calculateTotal']
984 2
			? "SELECT COUNT(*) as `total`, '$request[requestKey]' as `{$this->requestKeyField}`, '$request[resultKey]_total' as `{$this->resultKeyField}` FROM $table t $join $where"
985 10
			: null;
986 10
	}
987
988
	/**
989
	 * extract the join clause from the filters provided
990
	 * @param array $links  the set of link fields in the table
991
	 * @param array $filterKeys  the set of fields used in the filtering
992
	 * @return string  the join clause
993
	 */
994 10
	private function extractJoinClause($links, $filterKeys)
995
	{
996 10
		$join = ['INNER JOIN dds_object `o` on `t`.`_uuid` = `o`.`_uuid`'];
997 10
		foreach ($links as $l) {
998 4
			if (in_array($l, $filterKeys))
999 4
				$join[] = "LEFT JOIN dds_link `link_$l` ON (`link_$l`.`from_id`=`o`.`_uuid` AND `link_$l`.`from_member`='$l')";
1000
		}
1001 10
		return implode(' ', $join);
1002
	}
1003
1004
	/**
1005
	 * extract the where clause from the filters provided
1006
	 * @param array $filters
1007
	 * @param boolean $includeDeleted  true if including deleted
1008
	 * @param array $links  any filter keys that are links
1009
	 * @param array &$filterKeys  the set of keys used as filters for use elsewhere
1010
	 * @return string
1011
	 */
1012 16
	private function extractWhereClause($filters, $includeDeleted, $links, &$filterKeys)
1013
	{
1014 16
		$filterKeys = [];
1015 16
		$notDeleted = '`o`.`_deleted`=0';
1016 16
		if (count($filters)==0)
1017 6
			return $includeDeleted ? '' : "WHERE $notDeleted";
1018 14
		$this->createFilterClause($filters, $links, $filterKeys);
1019 8
		if (!$includeDeleted)
1020 8
			return "WHERE $notDeleted AND ($filters[sql])";
1021
		return "WHERE $filters[sql]";
1022
	}
1023
1024
	/**
1025
	 * recursively go through the filters and generate the sql filter
1026
	 * clause that's used for the where clause
1027
	 * @param array &$filters   the set of filters that are to be
1028
	 *   traversed to generate the overall where clause
1029
	 * @param array $links  the set of link members that aren't stored in this
1030
	 *   table
1031
	 * @param array &$filterKeys  the set of members that are being filtered on
1032
	 */
1033 14
	private function createFilterClause(&$filters, $links, &$filterKeys) {
1034
1035
		// filter down until we hit the actual filters
1036 14
		if (is_array($filters[0])) {
1037
1038 14
			foreach ($filters as $k => &$filter) {
1039 14
				if ($k === 'logic')
1040 8
					continue;
1041 14
				$this->createFilterClause($filter, $links, $filterKeys);
1042
			}
1043
			//
1044
			// to get here we're bubbling back up the recursion again
1045
			//
1046 14
			if (isset($filters[0]['itemSql'])) {
1047
				// ok, so now we should have itemSql's defined
1048
				// in which case we combine those with to get the filterSql
1049 14
				$filterSql = [];
1050 14
				foreach ($filters as $k => &$filter) {
1051 14
					if ($k === 'logic')
1052 8
						continue;
1053
					// use the logic keys if any for the items - these can be in position 3
1054
					// for most operators and position 2 for operators that don't take a key
1055 14
					if ($this->operatorTakesObject($filter[1])) {
1056 14
						if (isset($filter[3]))
1057 8
							$filterSql[$filter[3]] = $filter['itemSql'];
1058
						else
1059 14
							$filterSql[] = $filter['itemSql'];
1060
					} else {
1061 2
						if (isset($filter[2]))
1062 2
							$filterSql[$filter[2]] = $filter['itemSql'];
1063
						else
1064
							$filterSql[] = $filter['itemSql'];
1065
					}
1066
				}
1067 14
				if (empty($filters['logic']))
1068 6
					$filters['filterSql'] = $filters['sql'] = implode(' AND ', $filterSql);
1069
				else {
1070
					// make sure we test logic filters in order from longest key to shortest key
1071
					// otherwise we can end up with subkeys screwing things up
1072 8
					$orderedKeys = array_map('strlen', array_keys($filterSql));
1073 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

1073
					array_multisort($orderedKeys, /** @scrutinizer ignore-type */ SORT_DESC, $filterSql);
Loading history...
1074 8
					$this->checkLogic(array_keys($filterSql), $filters['logic']);
1075
1076
					// make sure that keys that are subsets of variables or other keys
1077
					// don't interfere by converting all to their md5 hash and doing a
1078
					// double conversion,
1079 2
					$keys = array_keys($filterSql);
1080 2
					$keys2hashed = array();
1081 2
					foreach ($keys as $k)
1082 2
						$keys2hashed[$k] = md5($k);
1083 2
					$logicPass1 = str_replace($keys, $keys2hashed, $filters['logic']);
1084 8
					$filters['filterSql'] = $filters['sql'] = str_replace($keys2hashed, array_values($filterSql), $logicPass1);
1085
				}
1086
			} else {
1087
				// or we have filterSql's defined in which case combine these
1088
				// to get the complete set of filters
1089 8
				$clauseSql = [];
1090 8
				foreach ($filters as &$filter) {
1091 8
					$clauseSql[] = $filter['filterSql'];
1092 8
					unset($filter['sql']);
1093
				}
1094 8
				$filters['sql'] = '('.implode(') OR (', $clauseSql).')';
1095
			}
1096 8
			return;
1097
		}
1098
		//
1099
		// To get here we're at the leaf of the recursion tree
1100
		// Start creating the sql. $filters[0,1] will have been canonicalised, $filters[2] is protected using PDO
1101
		// Not all queries have a [2] though so should be just the operator (e.g. IS NULL)
1102
		//
1103 14
		$filter = (in_array($filters[0], $links) ? "`link_{$filters[0]}`.`to_id`": $this->quoteField($filters[0]));
1104 14
		$filterKeys[$filters[0]] = $filters[0];
1105 14
		if ($this->operatorTakesObject($filters[1]) && isset($filters[2]))
1106 14
			$filters['itemSql'] = $filters['sql'] = "$filter ".$this->prepareForPDO($filters[1], $filters[2]);
1107
		else {
1108 2
			$filters['itemSql'] = $filters['sql'] = "$filter $filters[1]";
1109
		}
1110 14
	}
1111
1112 14
	private function prepareForPDO($operator, $value)
1113
	{
1114 14
		if (is_array($value)) {
1115 6
			$pdoValues = [];
1116 6
			if (!in_array($operator, ['=','!=','IN','NOT IN']))
1117
				throw new \InvalidArgumentException("Daedalus: Cannot pass an *array* of values with an operator of $operator");
1118 6
			foreach ($value as $v) {
1119 6
				if (is_array($v))
1120
					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

1120
					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...
1121 6
				$count = count($this->boundValues);
1122 6
				$variable = ":var{$count}end";
1123 6
				$this->boundValues[$variable] = $v;
1124 6
				$pdoValues[] = $variable;
1125
			}
1126
			// converts equals to ins and outs
1127 6
			if ($operator == '=')
1128 6
				$operator = 'IN';
1129 6
			if ($operator == '!=')
1130 6
				$operator = 'NOT IN';
1131 6
			return "$operator (".implode(',',$pdoValues).')';
1132
		} else {
1133 10
			$count = count($this->boundValues);
1134 10
			$variable = ":var{$count}end";
1135 10
			if (strpos(strtolower($operator), 'like')!==false) {
1136 2
				$this->boundValues[$variable] = "%$value%";
1137 2
				return "$operator $variable";
1138 10
			} else if (strpos(strtolower($operator), 'null') !== false) {
1139
				return "$operator";
1140
			} else {
1141 10
				$this->boundValues[$variable] = $value;
1142 10
				return "$operator $variable";
1143
			}
1144
		}
1145
	}
1146
1147
	/**
1148
	 * Extract an order clause from the canonicalised order array
1149
	 * @param array $order
1150
	 * @return string
1151
	 */
1152 10
	private function extractOrderClause($order)
1153
	{
1154 10
		if (count($order)==0)
1155 8
			return '';
1156 4
		$clause = [];
1157 4
		foreach ($order as $o => $d) {
1158
			// if any of the order clauses are RAND then replace whole clause
1159 4
			if ($o === 'RAND') {
1160
				$clause = ["RAND()"];
1161
				break;
1162
			}
1163
			// allow negative ordering in mysql for nulls last
1164 4
			if (strpos($o, '-')===0)
1165 2
				$clause[] = '-'.substr($o, 1)." $d";
1166
			else
1167 2
				$clause[] = "$o $d";
1168
		}
1169 4
		return 'ORDER BY '.implode(', ',$clause);
1170
	}
1171
1172
	/**
1173
	 * Extract a limit clause from the canonicalised limit array
1174
	 * @param array $limit
1175
	 * @return string
1176
	 */
1177 10
	private function extractLimitClause($limit)
1178
	{
1179 10
		if (count($limit)==0)
1180
			return '';
1181 10
		return "LIMIT $limit[start], $limit[length]";
1182
	}
1183
1184
	/**
1185
	 * Insert a series of links from object a to set of objects b
1186
	 *
1187
	 * @param string $fromLink - a UUID64
1188
	 * @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...
1189
	 *   member_ref keys are set for (so we can distinguish between not set for
1190
	 *   updates (=> do nothing) and set but blank (=> delete)
1191
	 * @return integer - the number of inserted links
1192
	 */
1193 66
	private function setMemberLinks($fromLink, array $toMemberLinks)
1194
	{
1195
		// check you've been supplied with rinky dinky data
1196 66
		if (!$this->isUUID($fromLink))
1197
			throw new \InvalidArgumentException("The fromLink should be a UUID64. You passed in $fromLink");
1198 66
		$count = 0;
1199 66
		foreach ($toMemberLinks as $member => $toLinks) {
1200 6
			if (!is_array($toLinks))
1201
				$toLinks = [$toLinks];
1202 6
			if (!$this->areUUIDs($toLinks))
1203
				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

1203
				throw new \InvalidArgumentException("The toMemberLinks should be UUID64s. You passed in "./** @scrutinizer ignore-type */ print_r($toLinks,true));
Loading history...
1204 6
			array_unique($toLinks);
1205
1206
			//
1207
			// clear any existing links from from_id:member to any of the to_ids
1208
			//
1209
			// There is an ambiguity here with what to do about soft-deleted objects that
1210
			// are linked. You could leave these here, but then if the other object
1211
			// was undeleted, then would you expect or be surprised if those links reappeared?
1212
			// I don't think there is any correct policy here so choosing to delete the links
1213
			//
1214 6
			DdsLink::deleteAll(['from_id'=>$fromLink, 'from_member'=>$member]);
1215
1216
			// add the new links in
1217 6
			$batchInsert = [];
1218 6
			$batchCount = count($toLinks);
1219 6
			foreach ($toLinks as $toLink)
1220 6
				$batchInsert[] = ['from_id'=>$fromLink, 'from_member'=>$member, 'to_id'=>$toLink];
1221 6
			$memberCount = 0;
1222 6
			if (count($batchInsert))
1223 6
				$memberCount = neon()->db->createCommand()->batchInsert('{{%dds_link}}', ['from_id','from_member','to_id'], $batchInsert)->execute();
1224 6
			if (YII_DEBUG && $memberCount !== $batchCount)
1225
				throw new Exception("The link insertion failed for member $member - $batchCount items should have been inserted vs $memberCount actual");
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsManager\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
1226 6
			$count += $memberCount;
1227
		}
1228 66
		return $count;
1229
	}
1230
1231
	/**
1232
	 * Get all member links associated with this object
1233
	 *
1234
	 * @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...
1235
	 * @return array  an array of all object ids linked against
1236
	 *   a member of this object e.g. ['author_id']=>[ blogIds ]
1237
	 */
1238 28
	private function getMemberLinks($uuid)
1239
	{
1240 28
		$membersLinks = $this->getMembersLinks([$uuid]);
1241 28
		return $membersLinks[$uuid];
1242
	}
1243
1244
	/**
1245
	 * Get the membersLinks associated with particular fields on objects
1246
	 * @param array $objectIds  - the set of objects you want the fields for
1247
	 * @return array  an array of [objectId]=>[member_ref=>links]. All objects
1248
	 *   passed in have an array returned even if that is an empty array
1249
	 */
1250 38
	protected function getMembersLinks(array $objectIds)
1251
	{
1252 38
		if (empty($objectIds))
1253 16
			return [];
1254 36
		$objectIds = array_unique($objectIds);
1255 36
		sort($objectIds, SORT_STRING);
1256 36
		$cacheKey = md5(serialize($objectIds));
1257 36
		if (!array_key_exists($cacheKey, self::$_linkResultsCache)) {
1258 36
			$rows = DdsLink::find()->noCache()->select(['from_id', 'from_member', 'to_id', '_deleted'])
1259 36
				->join('LEFT JOIN', '{{%dds_object}}', 'to_id=_uuid')
1260 36
				->where('`_deleted` IS NULL OR `_deleted` = 0')
1261 36
				->andWhere(['from_id'=>$objectIds])
1262 36
				->asArray()
1263 36
				->all();
1264 36
			$multilinks = array_fill_keys($objectIds, []);
1265 36
			foreach ($rows as $row)
1266 6
				$multilinks[$row['from_id']][$row['from_member']][] = $row['to_id'];
1267 36
			self::$_linkResultsCache[$cacheKey] = $multilinks;
1268
		}
1269 36
		return self::$_linkResultsCache[$cacheKey];
1270
	}
1271
1272
	/**
1273
	 * Send a change log entry to the change log manager
1274
	 * @param DdsClass $class  the DdsClass Object
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsManager\DdsClass 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...
1275
	 * @param string $changeKey  the change key for the action performed e.g. EDIT
1276
	 * @param string $objectUuid  the object uuid
1277
	 * @param array $changes  what the changes are
1278
	 * @return string  the uuid of the log entry
1279
	 */
1280 10
	private function createChangeLogEntry($class, $changeKey, $objectUuid, $originalValues=[], $newValues=[])
1281
	{
1282 10
		static $changeLog = null;
1283 10
		if (!$changeLog)
1284 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...
1285 10
		return $changeLog->addLogEntry($objectUuid, $class->toArray(), $changeKey, $originalValues, $newValues);
1286
	}
1287
1288
	/**
1289
	 * Check that the uuids provided are all of the given class type.
1290
	 * Remove any that aren't.
1291
	 * @param array $uuids  the set of uuids
1292
	 * @param string $classType  the class type they should all belong to
1293
	 * @return array  the set of uuids that all belong to this class type
1294
	 */
1295 4
	private function checkUuidsAgainstClassType($uuids, $classType)
1296
	{
1297
		// remove any objects that aren't of the same type as the class type
1298 4
		$db = neon()->db;
1299 4
		$uuidClause = [];
1300 4
		$objectValues = [];
1301 4
		$count = 0;
1302 4
		foreach ($uuids as $id) {
1303 4
			$placeHolder = ":v$count";
1304 4
			$uuidClause[] = $placeHolder;
1305 4
			$objectValues[$placeHolder] = $id;
1306 4
			$count++;
1307
		}
1308 4
		$uuidClause = '('.implode(',', $uuidClause).')';
1309 4
		$query = "SELECT [[_uuid]] FROM [[dds_object]] WHERE [[_class_type]]='$classType' AND [[_uuid]] IN $uuidClause";
1310 4
		$checkedUuidResults = $db->createCommand($query)->bindValues($objectValues)->queryAll();
1311 4
		return array_column($checkedUuidResults, '_uuid');
1312
	}
1313
1314
}
1315