Passed
Push — develop ( 44d21a...7d51f9 )
by Neill
34:25 queued 18:25
created

DdsObjectManager::setMemberLinks()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 2
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
c 0
b 0
f 0
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\DdsObject;
15
use neon\daedalus\services\ddsManager\models\DdsLink;
16
17
class DdsObjectManager
18
extends DdsCore
19
implements IDdsObjectManagement
20
{
21
22
	/** ---------------------------------------------------- */
23
	/** ---------- interface IDdsObjectManagement ---------- */
24
	/** ---------------------------------------------------- */
25
26
	/**
27
	 * Caches for results from queries so can protect against over-zealous
28
	 * requests to the database within one call
29
	 * @var array
30
	 */
31
	private static $_requestResultsCache = [];
32
	private static $_linkResultsCache = [];
33
	private static $_mapResultsCache = [];
34
35
	/**
36
	 * @inheritdoc
37
	 */
38 16
	public function addObjectRequest($classType, array $filters = [], array $order = [], array $limit = [], $resultKey = null, $includeDeleted = false)
39
	{
40
		// protect ourselves against SQL injection by making sure all
41
		// items are canonicalised as required (and use of PDO later)
42 16
		$classType = $this->canonicaliseRef($classType);
43 16
		$filters = $this->canonicaliseFilters($filters);
44 16
		$order = $this->canonicaliseOrder($order);
45 16
		$limit = $this->canonicaliseLimit($limit, $total, $calculateTotal);
46
47
		$request = [
48 16
			'classType' => $classType,
49 16
			'filters' => $filters,
50 16
			'order' => $order,
51 16
			'limit' => $limit,
52 16
			'total' => $total,
53 16
			'calculateTotal' => $calculateTotal,
54 16
			'deleted' => $includeDeleted
55
		];
56
		// add a request key so we can cache request results more easily
57 16
		$request['requestKey'] = md5(serialize($request));
58 16
		$result = $this->createSqlAndStoreRequest($request, $resultKey);
59 10
		return $result;
60
	}
61
62
	/**
63
	 * @inheritdoc
64
	 */
65 6
	public function getObjectRequests()
66
	{
67
		return [
68 6
			'requests' => $this->objectRequests,
69 6
			'boundValues' => $this->boundValues
70
		];
71
	}
72
73
	/**
74
	 * @inheritdoc
75
	 */
76 10
	public function hasObjectRequests()
77
	{
78 10
		return !empty($this->objectRequests);
79
	}
80
81
	/**
82
	 * @inheritdoc
83
	 */
84 10
	public function commitRequests()
85
	{
86
		// return an empty array if there is nothing to do.
87 10
		if (!$this->hasObjectRequests()) {
88
			profile_end('DDS::COMMIT_REQUESTS', 'dds');
89
			return [];
90
		}
91
92 10
		profile_begin('DDS::COMMIT_REQUESTS', 'dds');
93
94
		// map resultkeys to requestKeys for caching
95 10
		$resultKey2RequestKey = [];
96
97
		// get already cached results or create the query
98 10
		$query = '';
0 ignored issues
show
Unused Code introduced by
The assignment to $query is dead and can be removed.
Loading history...
99 10
		$sql = [];
100 10
		$data = [];
101 10
		$newData = [];
102 10
		foreach ($this->objectRequests as $requests) {
103 10
			foreach ($requests as $k => $r) {
104 10
				if (array_key_exists($r['requestKey'], self::$_requestResultsCache)) {
105 2
					$data[$k] = self::$_requestResultsCache[$r['requestKey']];
106
				} else {
107
					// set null result information about the request
108 10
					$newData[$k] = ['start' => $r['limit']['start'], 'length' => 0, 'total' => $r['total'], 'rows' => []];
109 10
					$sql[] = $r['sql'];
110 10
					if ($r['totalSql'])
111 2
						$sql[] = $r['totalSql'];
112 10
					$resultKey2RequestKey[$r['resultKey']] = $r['requestKey'];
113
				}
114
			}
115
		}
116
117
		// are there any new requests to make?
118 10
		if (count($sql)) {
119 10
			$query = implode('; ', $sql);
120
			// execute the query
121 10
			$reader = neon()->db->createCommand($query, $this->boundValues)->query();
122
			// store object ids ready to get multilinks afterwards
123 10
			$objectIds = [];
124
			// extract the results
125
			do {
126 10
				$results = $reader->readAll();
127 10
				foreach ($results as $r) {
128 10
					$resultKey = $r[$this->resultKeyField];
129 10
					$resultKey2RequestKey[$resultKey] = $r[$this->requestKeyField];
130 10
					unset($r[$this->resultKeyField]);
131 10
					if (strpos($resultKey, '_total') !== false) {
132 2
						$newData[substr($resultKey, 0, -6)]['total'] = $r['total'];
133
					} else {
134 10
						$objectIds[$r['_uuid']] = $r['_uuid'];
135 10
						$newData[$resultKey]['rows'][] = $r;
136 10
						$newData[$resultKey]['length']++;
137
					}
138
				}
139 10
			} while ($reader->nextResult());
140
141
			// get all links across the entire resultset
142 10
			$membersLinks = $this->getMembersLinks($objectIds);
143
144
			// convert any data to PHP format
145 10
			foreach ($newData as $k => &$d) {
146 10
				$this->convertResultsToPHP($d['rows'], $membersLinks);
147 10
				self::$_requestResultsCache[$resultKey2RequestKey[$k]] = $d;
148 10
				$data[$k] = $d;
149
			}
150
		}
151 10
		$this->lastCommitResults = $data;
152 10
		$this->clearRequests();
153
154 10
		profile_end('DDS::COMMIT_REQUESTS', 'dds');
155
156 10
		return $data;
157
	}
158
159
	/**
160
	 * @inheritdoc
161
	 */
162
	public function getLastCommitData($requestKey)
163
	{
164
		return isset($this->lastCommitResults[$requestKey]) ? $this->lastCommitResults[$requestKey] : [];
165
	}
166
167
	/**
168
	 * @inheritdoc
169
	 */
170 6
	public function runSingleObjectRequest($classType, array $filters = [], array $order = [], array $limit = [], $includeDeleted = false)
171
	{
172 6
		if (empty($classType))
173
			throw new \InvalidArgumentException('ClassType must be specified');
174
175
		// make sure that any currently queued requests from elsewhere are saved off the queue
176 6
		$currentRequests = $this->popRequests();
177
178
		// make the object request
179 6
		$requestId = $this->addObjectRequest($classType, $filters, $order, $limit, null, $includeDeleted);
180 6
		$data = $this->commitRequests();
181
182
		// and store them back again
183 6
		$this->pushRequests($currentRequests);
184
185 6
		return $data[$requestId];
186
	}
187
188
189
	/** ---------- Object Map Management ---------- **
190
191
	/**
192
	 * @inheritdoc
193
	 */
194 4
	public function getObjectMap($classType, $filters = [], $fields = [], $start = 0, $length = 1000, $includeDeleted = false)
195
	{
196 4
		return $this->getObjectMapManager()->getObjectMap($classType, $filters, $fields, $start, $length, $includeDeleted);
197
	}
198
199
	/**
200
	 * @inheritdoc
201
	 */
202 24
	public function makeMapLookupRequest($objectUuids, $mapFields = [], $classType = null)
203
	{
204 24
		return $this->getObjectMapManager()->makeMapLookupRequest($objectUuids, $mapFields, $classType);
205
	}
206
207
	/**
208
	 * @inheritdoc
209
	 */
210 10
	public function makeMapChainLookupRequest($objectUuids, $chainMembers = [], $chainMapFields = [], $inReverse = false)
211
	{
212 10
		return $this->getObjectMapManager()->makeMapChainLookupRequest($objectUuids, $chainMembers, $chainMapFields, $inReverse);
213
	}
214
215
216
	/**
217
	 * @inheritdoc
218
	 */
219 20
	public function commitMapRequests()
220
	{
221 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...
222
	}
223
224
	/**
225
	 * @inheritdoc
226
	 */
227 24
	public function getMapLookupResults($requestKey)
228
	{
229 24
		return $this->getObjectMapManager()->getMapLookupResults($requestKey);
230
	}
231
232
	/**
233
	 * @inheritdoc
234
	 */
235 10
	public function getMapChainLookupResults($requestKey)
236
	{
237 10
		return $this->getObjectMapManager()->getMapChainLookupResults($requestKey);
238
	}
239
240
	/**
241
	 * @inheritdoc
242
	 */
243 22
	public function clearMapRequests()
244
	{
245 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...
246
	}
247
248
	/** ---------- Basic Object Management ---------- **/
249
250
	/**
251
	 * @inheritdoc
252
	 */
253 16
	public function listObjects($classType, &$total = null, $includeDeleted = false, $inFull = true, $start = 0, $length = 100, $order = null)
254
	{
255 16
		$classType = $this->canonicaliseRef($classType);
256 16
		if (empty($order))
257 16
			$order = ['_created' => 'DESC'];
258 16
		$order = $this->canonicaliseOrder($order);
259
260
		// get the total first
261 16
		$class = null;
262 16
		$this->findClass($classType, $class, true);
263 16
		$total = $class->count_total;
264 16
		if (!$includeDeleted)
265 16
			$total -= $class->count_deleted;
266
267
		// now get the objects
268 16
		$query = (new \neon\core\db\Query)
269 16
			->from('{{dds_object}} o')
270 16
			->select($inFull ? "[[t]].*, [[o]].*" : "[[o]].*")
271 16
			->where(['[[o]].[[_class_type]]' => $classType])
272 16
			->offset($start)
273 16
			->limit($length);
274
275 16
		if (!$includeDeleted)
276 16
			$query->andWhere('[[o]].[[_deleted]] = 0');
277 16
		if ($inFull) {
278 16
			$tableName = $this->getTableFromClassType($classType);
279 16
			$query->innerJoin('{{' . $tableName . '}} t', '[[o]].[[_uuid]]=[[t]].[[_uuid]]');
280
		}
281
282 16
		if (is_array($order)) {
0 ignored issues
show
introduced by
The condition is_array($order) is always false.
Loading history...
283 16
			$orderBits = [];
284 16
			foreach ($order as $k => $d) {
285 16
				if ($k[1] == 'o') {
286 16
					$orderBits[] = "$k $d";
287 2
				} else if ($inFull) {
288 2
					$orderBits[] = "[[t]].$k $d";
289
				}
290
			}
291 16
			if ($orderBits)
292 16
				$orderClause = implode(', ', $orderBits);
293 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...
294
		}
295
296 16
		$data = $query->all();
297 16
		$this->convertResultsToPHP($data);
298 16
		return $data;
299
	}
300
301
	/**
302
	 * @inheritdoc
303
	 */
304 64
	public function addObject($classType, array $data, &$changeLogUuid = null)
305
	{
306 64
		$class = null;
307 64
		$classType = $this->canonicaliseRef($classType);
308 64
		$this->findClass($classType, $class, true);
309 64
		if ($class->hasChangeLog())
310 8
			$newValues = $data;
311 64
		$table = $this->getTableFromClassType($classType);
312
313
		// create the new object
314 64
		$object = new DdsObject;
315 64
		$now = date('Y-m-d H:i:s');
316 64
		$uuid = (!empty($data['_uuid']) && $this->isUUID($data['_uuid'])) ? $data['_uuid'] : $this->generateUUID();
317 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...
318 64
		$object->_class_type = $classType;
319 64
		$object->_created = $now;
320 64
		$object->_updated = $now;
321 64
		if (!$object->save())
322
			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

322
			throw new \RuntimeException("Couldn't create object: " . /** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
323
324
		// increment the class's object count
325 64
		$class->count_total += 1;
326 64
		$class->save();
327
328
		// then insert all of the associated data
329
		try {
330 64
			$this->convertFromPHPToDB($classType, $data, $links);
331 64
			$memberStorage = $this->getMemberStorage($classType);
332 64
			$count = 0;
333 64
			$fields = [];
334 64
			$inserts = [];
335 64
			$values = [];
336 64
			foreach (array_keys($memberStorage) as $memRef) {
337
				// save all non-empty fields.
338 64
				if (array_key_exists($memRef, $data) && $data[$memRef] !== null && $data[$memRef] !== '') {
339 58
					$fields[] = "`$memRef`";
340 58
					$inserts[] = ":v$count";
341 58
					$values[":v$count"] = $data[$memRef];
342 58
					$count++;
343
				}
344
			}
345
346 64
			if (count($fields))
347 58
				$query = "INSERT INTO `$table` (`_uuid`, " . implode(', ', $fields) . ") VALUES ('{$object->_uuid}', " . implode(', ', $inserts) . ")";
348
			else
349 6
				$query = "INSERT INTO `$table` (`_uuid`) VALUES ('{$object->_uuid}')";
350 64
			$command = neon()->db->createCommand($query);
351 64
			$command->bindValues($values);
352 64
			$command->execute();
353
354
			// if there are any links then add these to the link table
355 64
			$this->setMemberLinks($object->_uuid, $links);
356
357 64
			if ($class->hasChangeLog())
358 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

358
				$changeLogUuid = $this->createChangeLogEntry($class, 'ADD', /** @scrutinizer ignore-type */ $object->_uuid, [], $newValues);
Loading history...
359
		} catch (\Exception $e) {
360
			// execution failed so clean up the object
361
			$object->delete();
362
			$this->clearCaches();
363
			throw $e;
364
		}
365 64
		$this->clearCaches();
366
367 64
		$this->trigger("dds.afterAdd.$classType", new ObjectAddEvent([
368 64
			'classType' => $classType,
369 64
			'object' => array_merge($data, $object->toArray())
370
		]));
371 64
		return $object->_uuid;
372
	}
373
374
	/**
375
	 * @inheritdoc
376
	 */
377 4
	public function addObjects($classType, array $data, $chunkSize = 1000)
378
	{
379 4
		$classType = $this->canonicaliseRef($classType);
380 4
		$class = null;
381 4
		$this->findClass($classType, $class, true);
382 4
		$table = $this->getTableFromClassType($classType);
383 4
		$hasChangeLog = $class->hasChangeLog();
384
385
		// get a list of the columns
386 4
		$columns = [];
387 4
		$nullRow = [];
388 4
		$memberStorage = $this->getMemberStorage($classType);
389 4
		foreach (array_keys($memberStorage) as $memRef) {
390 4
			$columns[] = $memRef;
391 4
			$nullRow[$memRef] = null;
392
		}
393 4
		$columns[] = '_uuid';
394 4
		$nullRow['_uuid'] = null;
395
396 4
		foreach (array_chunk($data, $chunkSize) as $chunkData) {
397 4
			$rows = [];
398 4
			$objectDbRows = [];
399 4
			$objectsLinks = [];
400 4
			$now = date('Y-m-d H:i:s');
401 4
			foreach ($chunkData as $row) {
402 4
				if ($hasChangeLog)
403 2
					$newValues = $row;
404
				// ddt table value - format row data and append the uuid
405 4
				$this->convertFromPHPToDB($classType, $row, $links);
406 4
				$uuid = (!empty($row['_uuid']) && $this->isUUID($row['_uuid'])) ? $row['_uuid'] : $this->generateUUID();
407 4
				$row['_uuid'] = $uuid;
408
				// only include data that belongs in the table and null value remainder
409 4
				$rows[] = array_replace($nullRow, array_intersect_key($row, $nullRow));
410
				// object table row value
411 4
				$objectDbRows[] = [$row['_uuid'], $classType, $now, $now];
412 4
				$objectsLinks[$row['_uuid']] = $links;
413 4
				if ($hasChangeLog)
414 2
					$this->createChangeLogEntry($class, 'ADD', $uuid, [], $newValues);
0 ignored issues
show
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

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

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

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

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

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

701
				throw new \RuntimeException("Couldn't undelete the object: " . /** @scrutinizer ignore-type */ print_r($object->errors, true));
Loading history...
702
703
			// and update the class object deleted count
704 20
			$class = null;
705 20
			if ($this->findClass($object['_class_type'], $class)) {
706 20
				$class->count_deleted = max(0, $class->count_deleted - 1);
707 20
				$class->save();
708
709
				// see if we need to store the change log
710 20
				if ($class->hasChangeLog())
711 6
					$changeLogUuid = $this->createChangeLogEntry($class, 'UNDELETE', $uuid);
712
			}
713 20
			$this->clearCaches();
714
		}
715 20
	}
716
717
	/**
718
	 * @inheritdoc
719
	 */
720 38
	public function destroyObject($uuid, &$changeLogUuid = null)
721
	{
722 38
		$object = null;
723 38
		if ($this->getObjectFromId($uuid, $object)) {
724 38
			$class = null;
725 38
			$this->findClass($object['_class_type'], $class);
726 38
			if ($class && $class->hasChangeLog())
727 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...
728
729
			// delete any object table data
730 38
			$table = $this->getTableFromClassType($object['_class_type']);
731 38
			neon()->db->createCommand()
732 38
				->delete($table, ['_uuid' => $uuid])
733 38
				->execute();
734
735
			// and the object row itself
736 38
			neon()->db->createCommand()
737 38
				->delete('dds_object', ['_uuid' => $uuid])
738 38
				->execute();
739
740
			// then if ok, decrement the class's object counts
741 38
			if ($class) {
742 38
				$class->count_total = max(0, $class->count_total - 1);
743 38
				if ($object['_deleted'] == 1)
744 20
					$class->count_deleted = max(0, $class->count_deleted - 1);
745 38
				$class->save();
746
			}
747
748
			// finally, delete any links from or to the object
749 38
			DdsLink::deleteAll(['or', ['from_id' => $uuid], ['to_id' => $uuid]]);
750
751
			// and note this is in the change log
752 38
			if ($class && $class->hasChangeLog())
753 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...
754
755 38
			$this->clearCaches();
756
		}
757 38
	}
758
759
	/**
760
	 * @inheritdoc
761
	 */
762 6
	public function destroyObjects(array $uuids)
763
	{
764 6
		$objects = $this->getObjectRowsFromIds($uuids);
765 6
		if (count($objects) == 0)
766
			return;
767
768 6
		$objectUuids = [];
769 6
		$objectsDeleted = [];
770 6
		$classUuids = [];
771 6
		foreach ($objects as $obj) {
772 6
			$objectUuids[$obj['_uuid']] = $obj['_uuid'];
773 6
			if ($obj['_deleted'] == 1)
774 6
				$objectsDeleted[$obj['_class_type']][$obj['_uuid']] = $obj['_uuid'];
775 6
			$classUuids[$obj['_class_type']][] = $obj['_uuid'];
776
		}
777
778
		// delete all of the objects from their appropriate class table
779 6
		foreach ($classUuids as $classType => $uuids) {
780 6
			$class = null;
781 6
			$this->findClass($classType, $class);
782 6
			$table = $this->getTableFromClassType($classType);
783
784
			// note the destroys in the change log
785
			// TODO 20200107 Make a bulk call for change log entries
786 6
			if ($class && $class->hasChangeLog()) {
787 2
				foreach ($uuids as $uuid) {
788 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...
789 2
					$this->createChangeLogEntry($class, 'DESTROY', $uuid, $data);
790
				}
791
			}
792
793
			// now delete all of the objects
794 6
			neon()->db->createCommand()
795 6
				->delete($table, ['_uuid' => $uuids])
796 6
				->execute();
797
798
			// decrement the class's object counts
799 6
			if ($class) {
800 6
				$class->count_total = max(0, $class->count_total - count($uuids));
801 6
				if (isset($objectsDeleted[$classType]))
802 6
					$class->count_deleted = max(0, $class->count_deleted - count($objectsDeleted[$classType]));
803 6
				$class->save();
804
			}
805
		}
806
807
		// delete all of the objects from the DdsObject table
808 6
		DdsObject::deleteAll(['_uuid' => $objectUuids]);
809
810
		// finally, delete any links from or to the objects
811 6
		DdsLink::deleteAll(['or', ['from_id' => $objectUuids], ['to_id' => $objectUuids]]);
812
813 6
		$this->clearCaches();
814 6
	}
815
816
817
	/** -------------------------------------- **/
818
	/** ---------- Internal Methods ---------- **/
819
820
	/**
821
	 * @var \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
822
	 */
823
	private $_objectMapManager = null;
824
825
	/**
826
	 * @return \neon\daedalus\services\ddsManager\DdsObjectMapManager|null
827
	 */
828 28
	private function getObjectMapManager()
829
	{
830 28
		if (!$this->_objectMapManager)
831 28
			$this->_objectMapManager = new DdsObjectMapManager;
832 28
		return $this->_objectMapManager;
833
	}
834
835
	/**
836
	 * convert a row of data from the database to php
837
	 * @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...
838
	 * @param string $classTypeKey the key in the data that will give the class type
839
	 */
840 20
	private function convertResultsToPHP(&$dataSet, $links = null, $classTypeKey = '_class_type')
841
	{
842
		// if we haven't been provided any multilinks (even if empty array)
843
		// then see if there are any multilinks to extract
844 20
		if ($links === null) {
845 16
			$objectIds = [];
846 16
			foreach ($dataSet as $row)
847 14
				$objectIds[] = $row['_uuid'];
848 16
			$links = $this->getMembersLinks($objectIds);
849
		}
850 20
		foreach ($dataSet as &$data) {
851
			// convert full rows to database. Totals are left untouched
852 18
			if (isset($data[$classTypeKey]))
853 18
				$this->convertFromDBToPHP($data, $links[$data['_uuid']], $classTypeKey);
854
		}
855 20
	}
856
857
	/**
858
	 * Get the model object from its id
859
	 * @param string $uuid  the object id
860
	 * @param DdsObject &$object  the returned object
861
	 * @return boolean  whether or not object found
862
	 */
863 50
	private function getObjectFromId($uuid, &$object = null)
864
	{
865 50
		$object = DdsObject::find()->noCache()->where(['_uuid' => $uuid])->one();
866 50
		return ($object !== null);
867
	}
868
869
	/**
870
	 * Get the array data for a row from its id
871
	 * @param array $uuids  an array of object uuids
872
	 * @return array  the objects found
873
	 */
874 8
	private function getObjectRowsFromIds(array $uuids)
875
	{
876 8
		$uuids = array_unique($uuids);
877 8
		$rows = DdsObject::find()->noCache()->where(['_uuid' => $uuids])->asArray()->all();
878 8
		return $rows;
879
	}
880
881
	/**
882
	 * @var array  values bound for the next commit
883
	 */
884
	private $boundValues = [];
885
886
	/**
887
	 * @var string  internal name for request keys
888
	 */
889
	private $requestKeyField = "___request_key";
890
891
	/**
892
	 * @var string  internal name for result keys
893
	 */
894
	private $resultKeyField = "___result_key";
895
896
	/**
897
	 * @var array  store of the last commit
898
	 */
899
	private $lastCommitResults = [];
900
901
	/**
902
	 * @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...
903
	 */
904
	private $objectRequests = [];
905
906
	/**
907
	 * Convert and store an object request
908
	 * @param array $request  the request
909
	 * @param string $resultKey  if provided use this else create one
910
	 * @return string  return the resultKey
911
	 */
912 16
	private function createSqlAndStoreRequest(array $request, $resultKey = null)
913
	{
914
		// track number of calls to function to generate unique wthin one php request per call and per request result keys
915 16
		static $counter = 0;
916 16
		$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 68
	private function clearCaches()
940
	{
941 68
		self::$_requestResultsCache = [];
942 68
		self::$_linkResultsCache = [];
943 68
		self::$_mapResultsCache = [];
944 68
	}
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 14
			foreach ($filters as $k => &$filter) {
1038 14
				if ($k === 'logic')
1039 8
					continue;
1040 14
				$this->createFilterClause($filter, $links, $filterKeys);
1041
			}
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
	 * Set member links in bulk into the links table
1186
	 * @param array $objectsLinks  an array of $fromUuid => $toMemberLinks
1187
	 *   where uuid is the from uuid and $toMemberLinks is an array of
1188
	 */
1189 68
	private function setMembersLinks($objectsLinks, $chunkSize)
1190
	{
1191 68
		if (count($objectsLinks) === 0)
1192
			return 0;
1193 68
		$totalCount = 0;
1194 68
		$db = neon()->db;
1195 68
		$ddsLink = DdsLink::tableName();
1196 68
		foreach (array_chunk($objectsLinks, $chunkSize, true) as $chunkData) {
1197 68
			$count = 0;
1198 68
			$batchInsert = [];
1199 68
			$deleteLinkClauses = [];
1200 68
			foreach ($chunkData as $fromUuid => $toMemberLinks) {
1201
				// check you've been supplied with rinky dinky data
1202 68
				if (!$this->isUUID($fromUuid))
1203
					throw new \InvalidArgumentException("The fromUuid should be a UUID64. You passed in $fromUuid");
1204 68
				foreach ($toMemberLinks as $member => $toLinks) {
1205 8
					if (!is_array($toLinks))
1206
						$toLinks = [$toLinks];
1207 8
					if (!$this->areUUIDs($toLinks))
1208
						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

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