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

DdsCore::isUUID()   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
eloc 1
dl 0
loc 3
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
ccs 2
cts 2
cp 1
crap 1
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\interfaces\IDdsBase;
11
12
use neon\daedalus\services\ddsManager\models\DdsClass;
13
use neon\daedalus\services\ddsManager\models\DdsDataType;
14
use neon\daedalus\services\ddsManager\models\DdsMember;
15
use neon\daedalus\services\ddsManager\models\DdsStorage;
16
use neon\daedalus\services\ddsManager\models\DdsObject;
17
use \yii\base\Component;
18
19
20
use neon\core\helpers\Hash;
21
22
class DdsCore extends Component implements IDdsBase
23
{
24
	const MAX_LENGTH = 1000;
25
	const MYSQL_DEFAULT_COLLATION = "CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci";
26
27
	/** ---------- IDdsBase Methods ---------- **/
28
29
	/** ---------- Utility Methods ---------- **/
30
31
	/**
32
	 * @inheritdoc
33
	 */
34 2
	public function canonicalise($reference)
35
	{
36 2
		return $this->canonicaliseRef($reference);
37
	}
38
39
	/**
40
	 * @inheritdoc
41
	 */
42 70
	public function listStorageTypes()
43
	{
44 70
		return DdsStorage::find()->asArray()->all();
45
	}
46
47
	/**
48
	 * @inheritdoc
49
	 */
50 82
	public function now()
51
	{
52 82
		return date('Y-m-d H:i:s');
53
	}
54
55
	/** -------------------------------------- **/
56
	/** ---------- Protected Methods --------- **/
57
	/** -------------------------------------- **/
58
59
	/**
60
	 * store a set of migrations
61
	 * @param string $up
62
	 * @param string $down
63
	 * @return string  the migration id
64
	 */
65 90
	protected function storeMigration($up, $down)
66
	{
67 90
		return neon()->dds->IDdsAppMigrator->storeMigration($up, $down);
0 ignored issues
show
Bug introduced by
The method storeMigration() does not exist on null. ( Ignorable by Annotation )

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

67
		return neon()->dds->IDdsAppMigrator->/** @scrutinizer ignore-call */ storeMigration($up, $down);

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

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

Loading history...
Bug Best Practice introduced by
The property IDdsAppMigrator does not exist on neon\daedalus\App. Since you implemented __get, consider adding a @property annotation.
Loading history...
68
	}
69
70
	/**
71
	 * Remove a migration entry
72
	 *
73
	 * @param string $id  the id for the migration to be removed
74
	 */
75
	protected function removeMigration($id)
76
	{
77
		neon()->dds->IDdsAppMigrator->removeMigration($id);
0 ignored issues
show
Bug Best Practice introduced by
The property IDdsAppMigrator does not exist on neon\daedalus\App. Since you implemented __get, consider adding a @property annotation.
Loading history...
78
	}
79
80
	/**
81
	 * create a table for a particular class type
82
	 * @param type $classType
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsManager\type was not found. Maybe you did not declare it correctly or list all dependencies?

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

filter:
    dependency_paths: ["lib/*"]

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

Loading history...
83
	 */
84 84
	protected function createClassTable($classType)
85
	{
86 84
		$tableName = $this->getTableFromClassType($classType);
87
		// create the up and down sql migration code
88 84
		$upSql = $this->getCreateTableSql($tableName);
89 84
		$downSql = $this->getDropTableSql($classType);
90 84
		neon()->db->createCommand($upSql)->execute();
91 84
		$this->storeMigration($upSql, $downSql);
0 ignored issues
show
Bug introduced by
$downSql of type array<integer,string> is incompatible with the type string expected by parameter $down of neon\daedalus\services\d...sCore::storeMigration(). ( Ignorable by Annotation )

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

91
		$this->storeMigration($upSql, /** @scrutinizer ignore-type */ $downSql);
Loading history...
92 84
	}
93
94
	/**
95
	 * drops a table for a particular class type
96
	 * @param type $classType
97
	 */
98 28
	protected function dropClassTable($classType)
99
	{
100 28
		$tableName = $this->getTableFromClassType($classType);
101 28
		$upSql = $this->getDropTableSql($classType);
102 28
		$downSql = $this->getCreateTableSql($tableName);
103 28
		foreach ($upSql as $up)
104 28
			neon()->db->createCommand($up)->execute();
105 28
		$this->storeMigration($upSql, $downSql);
0 ignored issues
show
Bug introduced by
$upSql of type array<integer,string> is incompatible with the type string expected by parameter $up of neon\daedalus\services\d...sCore::storeMigration(). ( Ignorable by Annotation )

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

105
		$this->storeMigration(/** @scrutinizer ignore-type */ $upSql, $downSql);
Loading history...
106 28
	}
107
108 82
	protected function addClassMemberColumn($classType, $memberRef)
109
	{
110 82
		$storage = $this->getMemberStorage($classType, $memberRef);
111 82
		$table = $this->getTableFromClassType($classType);
112 82
		$member = $storage[$memberRef];
113
		// handle some storage differences
114 82
		$columnCheck = substr($member['column'], 0, 4);
115 82
		if ($columnCheck=='CHAR' || $columnCheck=='UUID') {
116 18
			$size = (isset($member['definition']['size'])) ? $member['definition']['size'] : 150;
117 18
			$member['column'] = str_replace($columnCheck, "CHAR($size)", $member['column']);
118
		}
119
		try {
120 82
			$upMember = "ALTER TABLE `$table` ADD `$memberRef` $member[column];";
121 82
			$downMember = "ALTER TABLE `$table` DROP `$memberRef`;";
122 82
			neon()->db->createCommand($upMember)->execute();
123 82
			$this->storeMigration($upMember, $downMember);
124 82
			if ($member['index']) {
125 82
				$upIndex = "ALTER TABLE `$table` ADD ".$member['index']."(`$memberRef`);";
126 82
				$downIndex = "ALTER TABLE `$table` DROP INDEX `$memberRef`;";
127 82
				neon()->db->createCommand($upIndex)->execute();
128 82
				$this->storeMigration($upIndex, $downIndex);
129
			}
130 82
			return true;
131
		} catch (\Exception $e) {
132
			return $e->getMessage();
133
		}
134
	}
135
136 26
	protected function dropClassMemberColumn($classType, $memberRef)
137
	{
138 26
		$table = $this->getTableFromClassType($classType);
139 26
		$storage = $this->getMemberStorage($classType, $memberRef);
140 26
		$member = $storage[$memberRef];
141
		try {
142 26
			if ($storage[$memberRef]['index']) {
143 26
				$upIndex = "ALTER TABLE `$table` DROP INDEX `$memberRef`;";
144 26
				$downIndex = "ALTER TABLE `$table` ADD ".$member['index']."(`$memberRef`);";
145 26
				neon()->db->createCommand($upIndex)->execute();
146 26
				$this->storeMigration($upIndex, $downIndex);
147
			}
148 26
			$upMember = "ALTER TABLE `$table` DROP `$memberRef`;";
149 26
			$downMember = "ALTER TABLE `$table` ADD `$memberRef` $member[column];";
150 26
			neon()->db->createCommand($upMember)->execute();
151 26
			$this->storeMigration($upMember, $downMember);
152
		} catch (\Exception $e) {
153
			return $e->getMessage();
154
		}
155 26
	}
156
157
	/**
158
	 * get the storage table for each member ref defined in the class type
159
	 * @param string $classType
160
	 * @param string $memberRef  if set then just get that value
161
	 * @return []  array of ['member_ref']=>['data_type_ref', 'column', 'index']
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...
162
	 *  where column is the db column type
163
	 */
164 84
	protected function getMemberStorage($classType, $memberRef=null)
165
	{
166 84
		$boundValues = [];
167
		$query =<<<EOQ
168 84
SELECT `m`.`member_ref`, `s`.`type`, d.`definition` FROM dds_member m
169
JOIN `dds_data_type` d ON `m`.`data_type_ref`=d.`data_type_ref`
170
JOIN `dds_storage` s ON `d`.`storage_ref`=s.`storage_ref`
171
WHERE m.`class_type`=:classType;
172
EOQ;
173 84
		$boundValues[':classType'] = $classType;
174 84
		if ($memberRef) {
175 82
			$query .= " AND `m`.`member_ref`=:memberRef";
176 82
			$boundValues[':memberRef'] = $memberRef;
177
		}
178
179 84
		$cmd = neon()->db->createCommand($query, $boundValues);
180 84
		$rows = $cmd->queryAll();
181 84
		$memberStorage = [];
182 84
		foreach ($rows as $r) {
183 84
			$memberStorage[$r['member_ref']] = [
184 84
				'column' => $this->getColumnType($r['type']),
185 84
				'index' => $this->getIndexType($r['type']),
186 84
				'definition' => empty($r['definition']) ? null : json_decode($r['definition'],true)
187
			];
188
		}
189 84
		return $memberStorage;
190
	}
191
192 84
	protected function getColumnType($storageType)
193
	{
194 84
		$type = "";
195 84
		switch (strtoupper($storageType)) {
196 84
			case 'INTEGER_TINY': $type = "TINYINT"; break;
197 84
			case 'INTEGER_SHORT': $type = "SMALLINT"; break;
198 84
			case 'INTEGER': $type = "INT"; break;
199 84
			case 'INTEGER_LONG': $type = "BIGINT"; break;
200 84
			case 'FLOAT': $type = "FLOAT"; break;
201 84
			case 'DOUBLE': $type = "DOUBLE"; break;
202 84
			case 'DATE': $type = "DATE"; break;
203 84
			case 'DATETIME': $type = "DATETIME"; break;
204 84
			case 'TIME': $type = 'TIME'; break;
205 84
			case 'TEXT_SHORT': $type = "VARCHAR(150)"; break;
206 22
			case 'TEXT': $type = "TEXT"; break;
207 22
			case 'TEXT_LONG': $type = "MEDIUMTEXT"; break;
208 20
			case 'BINARY_SHORT': $type = "BLOB"; break;
209 20
			case 'BINARY': $type = "MEDIUMBLOB"; break;
210 20
			case 'BINARY_LONG': $type = "LONGBLOB"; break;
211 20
			case 'CHAR': $type = "CHAR"; break;
212 12
			case 'UUID': $type = "UUID"; break;
213
			default: $type="UNKNOWN STORAGE TYPE $storageType"; break;
214
		}
215 84
		$collation = $this->getCollation($storageType);
216 84
		if ($collation)
217 12
			return "$type $collation DEFAULT NULL ";
218 82
		return "$type DEFAULT NULL ";
219
	}
220
221 84
	protected function getIndexType($storageType)
222
	{
223 84
		$index = "";
224 84
		switch (strtoupper($storageType)) {
225 84
			case 'INTEGER_TINY':
226 84
			case 'INTEGER_SHORT':
227 84
			case 'INTEGER':
228 84
			case 'INTEGER_LONG':
229 84
			case 'FLOAT':
230 84
			case 'DOUBLE':
231 84
			case 'DATE':
232 84
			case 'DATETIME':
233 84
			case 'TIME':
234 84
			case 'TEXT_SHORT':
235 22
			case 'CHAR':
236 16
			case 'UUID':
237 84
				$index = "INDEX";
238 84
			break;
239 4
			case 'TEXT':
240 2
			case 'TEXT_LONG':
241
			case 'BINARY_SHORT':
242
			case 'BINARY':
243
			case 'BINARY_LONG':
244
			default:
245 4
				$index = null;
246 4
			break;
247
		}
248 84
		return $index;
249
	}
250
251 86
	protected function getCollation($storageType)
252
	{
253 86
		$collation = null;
254
		switch ($storageType) {
255 86
			case 'UUID':
256 84
				$collation = "CHARACTER SET latin1 COLLATE latin1_general_cs";
257 84
			break;
258
		}
259 86
		return $collation;
260
	}
261
262
	/**
263
	 * A cache of class metadata
264
	 * @var array
265
	 */
266
	private static $_classCache;
267
268
	/**
269
	 * A cache of class member metadata
270
	 * @var array
271
	 */
272
	protected static $_classMembersCache;
273
274
	/**
275
	 * A cache of map members for classes
276
	 * @var array
277
	 */
278
	protected static $_classMemberMapCache;
279
280
	/**
281
	 * get hold of a class
282
	 * @param string $classType
283
	 * @param \neon\daedalus\services\ddsManager\models\DdsClass &$class
284
	 * @param boolean $throwException [false] Whether we should throw an exception if the class is not found
285
	 * @throws \InvalidArgumentException if class not found and $throwException is true
286
	 * @return boolean  whether or not found
287
	 */
288 86
	protected function findClass($classType, &$class=null, $throwException=false)
289
	{
290 86
		$ct = $this->canonicaliseRef($classType);
291 86
		if (empty(self::$_classCache[$ct])) {
292 86
			self::$_classCache[$ct] = DdsClass::findOne(['class_type' => $ct]);
293
		}
294 86
		$class = self::$_classCache[$ct];
295 86
		if (!$class && $throwException)
296
			throw new \InvalidArgumentException('Unknown class type "'.$ct.'"');
297 86
		return ($class !== null);
298
	}
299
300
	/**
301
	 * Clear the class database cache
302
	 * @param string $classType
303
	 */
304 50
	protected function clearClassCache($classType)
305
	{
306 50
		unset(self::$_classCache[$classType]);
307 50
		$this->clearClassMemberCache($classType);
308 50
	}
309
310
	/**
311
	 * Clear the class member cache
312
	 *
313
	 * @param string $classType
314
	 */
315 84
	protected function clearClassMemberCache($classType)
316
	{
317 84
		unset(static::$_classMembersCache[$classType]);
318 84
		unset(static::$_classMemberMapCache[$classType]);
319 84
	}
320
321
	/**
322
	 * get hold of a class member object
323
	 * @param string $classType the class type the member belongs to
324
	 * @param string $memberRef the member ref identifying this member in the class
325
	 * @param \neon\daedalus\services\ddsManager\models\DdsMember &$member
326
	 * @return boolean  whether or not found
327
	 */
328 34
	protected function findMember($classType, $memberRef, &$member)
329
	{
330 34
		$member = DdsMember::findOne(['class_type' => $classType, 'member_ref' => $memberRef]);
331 34
		return ($member != null);
332
	}
333
334
	/**
335
	 * @see IDdsClassManagement::listMembers
336
	 */
337 8
	protected function listMembersForClass($classType, $includeDeleted=false, $keyBy='member_ref')
338
	{
339 8
		if (!is_string($classType))
340
			throw new \InvalidArgumentException('The class type $classType parameter should be a string');
341 8
		$select = ['member_ref', 'label', 'data_type_ref', 'description', 'choices', 'map_field', 'link_class'];
342 8
		if (!empty($keyBy) && !in_array($keyBy, $select))
343
			throw new \InvalidArgumentException("Parameter keyBy must be empty or one of ".print_r($select,true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($select, 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

343
			throw new \InvalidArgumentException("Parameter keyBy must be empty or one of "./** @scrutinizer ignore-type */ print_r($select,true));
Loading history...
344
345
		// see if we have a cached version or getting from the database
346 8
		if (empty(static::$_classMembersCache[$classType][$includeDeleted])) {
347 8
			$query = DdsMember::find()->where(['class_type' => $classType]);
348 8
			if ($includeDeleted)
349 8
				$select[] = 'deleted';
350
			else
351 2
				$query->andWhere(['deleted' => 0]);
352 8
			$rows = $query->select($select)->orderBy('created')->asArray()->all();
353 8
			foreach ($rows as $k=>$r)
354 4
				$rows[$k]['choices'] = json_decode($r['choices'], true);
355 8
			static::$_classMembersCache[$classType][$includeDeleted] = $rows;
356
		}
357
358 8
		if (empty($keyBy))
359
			return static::$_classMembersCache[$classType][$includeDeleted];
360
361
		// key by a particular ref
362 8
		$results = [];
363 8
		foreach (static::$_classMembersCache[$classType][$includeDeleted] as $r)
364 4
			$results[$r[$keyBy]] = $r;
365
366 8
		return $results;
367
	}
368
369
370
	/**
371
	 * Get the map field for a class
372
	 * @param string $class
373
	 */
374 4
	protected function getMapMemberForClass($classType)
375
	{
376 4
		if (empty(static::$_classMemberMapCache[$classType])) {
377 4
			if ($this->findClass($classType, $class)) {
378 4
				$member = DdsMember::find()
379 4
					->where(['class_type' => $classType, 'map_field' => 1, 'deleted' => 0])
380 4
					->asArray()->limit(1)->one();
381 4
				$this->setClassMapMemberCache($classType, $member);
382
			}
383
		}
384 4
		return static::$_classMemberMapCache[$classType];
385
	}
386
387
	/**
388
	 * Set the map member for a class
389
	 * @param string $classType  the class
390
	 * @param string $member  its map member
391
	 */
392 4
	protected function setClassMapMemberCache($classType, $member)
393
	{
394 4
		static::$_classMemberMapCache[$classType]=$member;
395 4
	}
396
397
	/**
398
	 * get hold of a data type by its ref
399
	 * @param string $dataTypeRef
400
	 * @param \neon\daedalus\services\ddsManager\models\DdsDataType &$dataType
401
	 * @return boolean  whether or not found
402
	 */
403 84
	protected function findDataType($dataTypeRef, &$dataType=null)
404
	{
405 84
		$dataType = DdsDataType::findOne(['data_type_ref'=>$dataTypeRef]);
406 84
		return ($dataType !== null);
407
	}
408
409
	/**
410
	 * see if there are any objects for a particular class type
411
	 * @param string $classType
412
	 * @return boolean
413
	 */
414 28
	protected function hasObjects($classType)
415
	{
416 28
		$obj = DdsObject::findOne(['_class_type'=>$classType]);
417 28
		return !empty($obj);
418
	}
419
420
	/**
421
	 * canonicalise a reference so that it follows a set pattern
422
	 *
423
	 * This is to prevent problems with queries where the field name may be
424
	 * illegitimate and also to help prevent SQL injection in raw queries
425
	 *
426
	 * @param string $ref  the uncanonicalised reference
427
	 * @return string  the canonicalised one
428
	 */
429 98
	protected function canonicaliseRef($ref)
430
	{
431 98
		return preg_replace("/[^a-z0-9_]/", '', strtolower(preg_replace("/ +/", '_', trim($ref))));
432
	}
433
434
	/**
435
	 * canonicalise an array of refs. These are assumed to be of the
436
	 * form [key]=>$ref
437
	 * @param array $refs  an array of uncanonicalised refs
438
	 * @return array  the array of canonicalised ones
439
	 */
440
	protected function canonicaliseRefs(array $refs)
441
	{
442
		$canon = [];
443
		foreach ($refs as $key => $ref) {
444
			$canon[$key] = $this->canonicaliseRef($ref);
445
		}
446
		return $canon;
447
	}
448
449
	/**
450
	 * Canonicalise a reference by parts where each part is canonicalised separately
451
	 * e.g. abc.def can be canonicalised separated by the '.' character
452
	 *
453
	 * @param string $ref the reference to canonicalise
454
	 * @param char $separator  the single character separator to split the string into its parts
0 ignored issues
show
Bug introduced by
The type neon\daedalus\services\ddsManager\char 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...
455
	 * @return string  the canonicalised result
456
	 */
457 16
	protected function canonicaliseRefByParts($ref, $separator=".")
458
	{
459 16
		$parts = explode($separator, $ref);
460 16
		$canons = [];
461 16
		foreach ($parts as $p)
462 16
			$canons[] = $this->canonicaliseRef($p);
463 16
		return implode($separator, $canons);
464
	}
465
466
	/**
467
	 * Adds ` around column names as well as correctly prefixing dds_object columns
468
	 * @param $ref
469
	 * @return string
470
	 */
471 24
	protected function quoteField($ref, $ddsObjectAlias='o')
472
	{
473 24
		if (in_array($ref, ['_uuid', '_created', '_updated', '_class_ref'])) {
474 16
			return "`$ddsObjectAlias`.`$ref`";
475
		}
476 12
		return neon()->db->quoteColumnName($this->canonicaliseRefByParts($ref));
477
	}
478
479
	/**
480
	 * canonicalise the filters for a query
481
	 * @param array $filters  the uncanonicalised ones
482
	 * @return array  the canonicalised ones
483
	 */
484 16
	protected function canonicaliseFilters($filters)
485
	{
486 16
		if (!is_array($filters))
0 ignored issues
show
introduced by
The condition is_array($filters) is always true.
Loading history...
487
			return [];
488
		try {
489 16
			$this->canonicaliseFiltersRecursive($filters);
490
		} catch (\InvalidArgumentException $ex) {
491
			throw new \InvalidArgumentException($ex->getMessage()." Filters passed in: ".print_r($filters,true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($filters, 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

491
			throw new \InvalidArgumentException($ex->getMessage()." Filters passed in: "./** @scrutinizer ignore-type */ print_r($filters,true));
Loading history...
492
		}
493 16
		return $filters;
494
	}
495
496
	/**
497
	 * Filter recursively down through a set of filters until you find
498
	 * a filter clause and then canonicalise it
499
	 *
500
	 * @param array $filters
501
	 * @return null
502
	 */
503 16
	protected function canonicaliseFiltersRecursive(&$filters)
504
	{
505
		// is this a filter clause or set of filter clauses??
506 16
		if (!is_array($filters) || count($filters)==0)
0 ignored issues
show
introduced by
The condition is_array($filters) is always true.
Loading history...
507 14
			return;
508
509
		// recursively descend until one finds a filter clause
510 14
		if (is_array($filters[0])) {
511 14
			foreach ($filters as &$f)
512 14
				$this->canonicaliseFiltersRecursive($f);
513 14
			return;
514
		}
515
		// so canonicalise a filter clause
516 14
		if (array_key_exists(0, $filters))
517 14
			$this->canonicaliseFilter($filters[0],0);
518 14
		if (array_key_exists(1, $filters))
519 14
			$this->canonicaliseFilter($filters[1],1);
520
521
		// Handle nulls passed as values
522
		// ['field', '=', null] and ['field', '!=', null]
523
		// either this implementation or should throw an exception - otherwise you get a SQL error which can be
524
		// confusing - this attempts to give a clearer error message:
525 14
		if (array_key_exists(2, $filters) && $filters[2] === null) {
526
			throw new \InvalidArgumentException("You have passed null into the filter like: [$filters[0], $filters[1], null] if you want to compare null then use [$filters[0], 'is null'] or [$filters[0], 'is not null']");
527
			// it would be possible to adjust the query - but this may not be desired behaviour!
528
			// This would adjust the query for you so comparing on null would be ['field', '=', null]
529
			// if ($filters[1] === '=') $filters[1] = 'is null';
530
			// else if ($filters[1] === '!=') $filters[1] = 'is not null';
531
			// else throw \Exception('Incorrect filter null value passed');
532
		}
533 14
	}
534
535
	/**
536
	 * Canonicalise a part of the filters
537
	 * @param mixed $item
538
	 * @param integer $key
539
	 * @throws \InvalidArgumentException
540
	 */
541 14
	protected function canonicaliseFilter(&$item, $key)
542
	{
543 14
		if ($key === 0) {
544 14
			$item = $this->canonicaliseRefByParts($item);
545
		}
546 14
		if ($key === 1) {
547
			// accept only these operators
548 14
			switch(strtolower($item)) {
549 14
				case '=': case '!=':
550 10
				case '<': case '<=':
551 10
				case '>': case '>=':
552 14
				break;
553 4
				case 'in': case 'not in':
554 4
				case 'like': case 'not like':
555 2
				case 'is null': case 'is not null':
556 4
					$item = strtoupper($item);
557 4
				break;
558 2
				case 'null': case 'not null':
559
					// fix missing operator IS
560 2
					$item = 'IS '.strtoupper($item);
561 2
				break;
562
				default:
563
					throw new \InvalidArgumentException("Invalid comparison operator '$item' passed in filters");
564
			}
565
		}
566
567
		// $key == 2: values are handled through the use of PDO
568
		// $key == 3: keys are handled separately
569 14
	}
570
571
	/**
572
	 * Canonicalise the logic clause. This checks to see if all the keys
573
	 * are defined in the logic clause and that the logic clause doesn't contain
574
	 * any extraneous characters. Allowed additionals are AND, NOT, OR and ()
575
	 * @param string[] $keys
576
	 * @param string $logic
577
	 * @return string
578
	 */
579 8
	protected function checkLogic($keys, $logic)
580
	{
581
		// check there are no integer keys as this means not all keys are in the logic
582 8
		foreach ($keys as $k) {
583 8
			if ((int)$k === $k) {
584
				throw new \InvalidArgumentException("Daedalus: You have provided a logic string to the query, but it looks like not all filter clauses have a logic name added to them. All filter clauses need to represented in the logic statement.");
585
			}
586
		}
587
		// test is to remove all keys and allowed characters and see if anything is
588
		// left over. If so then there must be bad characters ... one assumes
589 8
		$subLogic = str_replace($keys, '', $logic);
590 8
		$subLogic = str_replace(
591 8
			['AND', 'and', 'NOT', 'not', 'OR', 'or', ' ', ')', '('],
592 8
			'', $subLogic
593
		);
594 8
		if (strlen($subLogic)>0)
595 6
			throw new \InvalidArgumentException("Daedalus: Invalid logic operator provided. Maybe you haven't defined all keys or have other logic than 'AND', 'OR', 'NOT' and '(',')' characters in your logic? You have defined the keys as ".print_r($keys, true)." for a logic statement of ".print_r($logic,true)." The remaining characters are ".print_r($subLogic,true));
0 ignored issues
show
Bug introduced by
Are you sure print_r($subLogic, 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

595
			throw new \InvalidArgumentException("Daedalus: Invalid logic operator provided. Maybe you haven't defined all keys or have other logic than 'AND', 'OR', 'NOT' and '(',')' characters in your logic? You have defined the keys as ".print_r($keys, true)." for a logic statement of ".print_r($logic,true)." The remaining characters are "./** @scrutinizer ignore-type */ print_r($subLogic,true));
Loading history...
Bug introduced by
Are you sure print_r($keys, 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

595
			throw new \InvalidArgumentException("Daedalus: Invalid logic operator provided. Maybe you haven't defined all keys or have other logic than 'AND', 'OR', 'NOT' and '(',')' characters in your logic? You have defined the keys as "./** @scrutinizer ignore-type */ print_r($keys, true)." for a logic statement of ".print_r($logic,true)." The remaining characters are ".print_r($subLogic,true));
Loading history...
Bug introduced by
Are you sure print_r($logic, 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

595
			throw new \InvalidArgumentException("Daedalus: Invalid logic operator provided. Maybe you haven't defined all keys or have other logic than 'AND', 'OR', 'NOT' and '(',')' characters in your logic? You have defined the keys as ".print_r($keys, true)." for a logic statement of "./** @scrutinizer ignore-type */ print_r($logic,true)." The remaining characters are ".print_r($subLogic,true));
Loading history...
596 2
		return $logic;
597
	}
598
599
	/**
600
	 * Determine if an SQL operator takes an object or not
601
	 * e.g. >,= etc do whereas NOT NULL doesn't
602
	 * @param type $operator
603
	 * @return boolean
604
	 */
605 14
	protected function operatorTakesObject($operator)
606
	{
607 14
		switch (strtolower($operator)) {
608 14
			case 'is not null': case 'is null':
609 2
				return false;
610
			break;
0 ignored issues
show
Unused Code introduced by
break is not strictly necessary here and could be removed.

The break statement is not necessary if it is preceded for example by a return statement:

switch ($x) {
    case 1:
        return 'foo';
        break; // This break is not necessary and can be left off.
}

If you would like to keep this construct to be consistent with other case statements, you can safely mark this issue as a false-positive.

Loading history...
611
			default:
612 14
				return true;
613
			break;
614
		}
615
	}
616
617
	/**
618
	 * canonicalise the order clause
619
	 * @param array $order an array of [key]=>'ASC|DESC'
620
	 * @return type
621
	 */
622 26
	protected function canonicaliseOrder($order)
623
	{
624 26
		$canon = [];
625 26
		if (is_array($order)) {
0 ignored issues
show
introduced by
The condition is_array($order) is always true.
Loading history...
626 26
			foreach ($order as $k=>$d) {
627 18
				$drn = strtoupper($d);
628 18
				switch ($drn) {
629 18
					case 'ASC': case 'DESC':
630
						// allow -ve key starts for nulls last in MySql
631 18
						if (strpos($k,'-') === 0)
632 2
							$canon['-'.$this->quoteField($k)] = $drn;
633
						else
634 18
							$canon[$this->quoteField($k)] = $drn;
635 18
					break;
636
					case 'RAND':
637
						$canon['RAND'] = 'RAND';
638
					break;
639
				}
640
			}
641
		}
642 26
		return $canon;
643
	}
644
645
	/**
646
	 * canonicalise the requested limit
647
	 * @param array $limit  the uncanonicalised limit
648
	 * @param integer &$total  the total value extracted from the limit
649
	 * @param bool &$calculateTotal  whether or not to calculate the total
650
	 * @return array  the canonicalised limit
651
	 */
652 16
	protected function canonicaliseLimit($limit, &$total, &$calculateTotal)
653
	{
654 16
		$canon = [];
655 16
		$total=null;
656 16
		$calculateTotal = false;
657 16
		if (is_array($limit)) {
0 ignored issues
show
introduced by
The condition is_array($limit) is always true.
Loading history...
658 16
			$canon = ['start'=>0,'length'=>self::MAX_LENGTH];
659 16
			foreach ($limit as $k=>$v) {
660 4
				$key = strtolower($k);
661 4
				switch ($key) {
662 4
					case 'start':
663 4
						$canon[$key] = (int) $v;
664 4
					break;
665 4
					case 'length':
666 4
						$canon[$key] = min((int) $v, self::MAX_LENGTH);
667 4
					break;
668 2
					case 'total':
669
						// $v can be truthy or the previous integer
670 2
						$total = is_numeric($v) ? (int) $v : null;
671 2
						$calculateTotal = ($v===true || $v==='true');
672 2
					break;
673
				}
674
			}
675 16
			$calculateTotal = ($calculateTotal && ($canon['start'] === 0 || $canon['start'] === '0'));
676
		}
677 16
		return $canon;
678
	}
679
680
	/**
681
	 * Convert row from the database to the formats required by PHP
682
	 * e.g. booleans from 1/0 to true/false
683
	 * @param [] $row  the row of data to be converted
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...
684
	 * @param string $classTypeKey the key in the data that will give the class type
685
	 */
686 36
	protected function convertFromDBToPHP(&$row, $links=[], $classTypeKey='_class_type')
687
	{
688 36
		$classType = $row[$classTypeKey];
689 36
		$members = $this->getClassMembers($classType);
690
		// now process only the member defined fields:
691 36
		foreach ($row as $key => &$value) {
692 36
			if (isset($members[$key])) {
693 36
				$memberLinks = isset($links[$key])?$links[$key]:[];
694 36
				$this->doConversionFromDBToPHP($members[$key], $value, $memberLinks);
695
			}
696
		}
697 36
	}
698
699
	/**
700
	 * The actual converter from DB to PHP. Override this if the data type is not
701
	 * handled here, and call this if not handled in the overridden method
702
	 * @param string $dataType the data type ref of the value
703
	 * @param mixed $value  the value returned by the database
704
	 */
705 36
	protected function doConversionFromDBToPHP($member, &$value, $memberLinks=[])
706
	{
707 36
		switch($member['data_type_ref']) {
708 36
			case 'choice':
709
				// silently ignore deleted old choice as no longer valid
710
				if (is_array($member['choices']) && isset($member['choices'][$value])) {
711
					$value = ['key'=>$value, 'value'=>$member['choices'][$value], 'type'=>'choice'];
712
				}
713
			break;
714 36
			case 'choice_multiple':
715
				$choices = json_decode($value, true);
716
				$value = [];
717
718
				// protect against non array values
719
				if (!empty($choices) && is_array($choices)) {
720
					foreach ($choices as $choice) {
721
						// silently ignore deleted old choice as no longer valid
722
						if (isset($member['choices'][$choice]))
723
							$value[] = ['key'=>$choice, 'value'=>$member['choices'][$choice]];
724
					}
725
				}
726
			break;
727 36
			case 'boolean':
728 2
				if ($value === NULL)
729
					return;
730 2
				$value = !!$value;
731 2
			break;
732 36
			case 'json': $value = json_decode($value, true); break;
733 36
			case 'link_multi':
734 36
			case 'file_ref_multi':
735 6
				$value = $memberLinks;
736 6
			break;
737
		}
738 36
	}
739
740
	/**
741
	 * convert data from PHP format to the database format
742
	 * @param string $classType
743
	 * @param array $data  the object data
744
	 * @param array &$links  on return, any links are extracted into this
745
	 */
746 66
	protected function convertFromPHPToDB($classType, &$data, &$links)
747
	{
748 66
		$links = [];
749 66
		$members = $this->getClassMembers($classType);
750 66
		foreach ($data as $key=>&$value) {
751 66
			$itemLinks = null;
752 66
			if (isset($members[$key])) {
753 66
				$this->doConversionFromPHPToDB($members[$key], $value, $itemLinks);
754 66
				if ($itemLinks !== null)
755 6
					$links[$key] = $itemLinks;
756
			}
757
		}
758 66
	}
759
760
	/**
761
	 * The actual converter from PHP to DB. Override this if the data type is not
762
	 * handled here, and call this if not handled in the overridden method
763
	 *
764
	 * @param array $member
765
	 * @param mixed $value
766
	 */
767 66
	protected function doConversionFromPHPToDB($member, &$value, &$links)
768
	{
769 66
		$links = null;
770 66
		switch($member['data_type_ref']) {
771 66
			case 'choice':
772
				// convert from the value array to the key if the array
773
				// the array was returned
774
				if (is_array($value) && isset($value['key']))
775
					$value = $value['key'];
776
			break;
777 66
			case 'choice_multiple':
778
				$value = json_encode($value);
779
			break;
780 66
			case 'boolean':
781
				// check for null values
782 2
				if ($value === null)
783
					return;
784
				// convert from truthy to database 1 or 0
785 2
				$value = $value ? 1 : 0;
786 2
			break;
787 66
			case 'json';
788
				// json encode the PHP object / array
789 2
				$value = json_encode($value);
790 2
			break;
791 66
			case 'link_multi':
792 64
			case 'file_ref_multi':
793
				// extract out the links so they can be saved separately
794 6
				$links = empty($value) ? [] : $value;
795 6
				$value = null;
796 6
			break;
797
		}
798 66
	}
799
800
	/**
801
	 * get hold of all of the class members given a classType
802
	 * @param string $classType
803
	 * @param array $dataTypes  add to restrict members to certain types
804
	 * @return array the members
805
	 */
806 66
	protected function getClassMembers($classType, array $dataTypes=[])
807
	{
808 66
		static $_classMembers = [];
809 66
		if (!array_key_exists($classType, $_classMembers)) {
810
			try {
811
				// get the members and convert to array below to
812
				// make sure model afterFind has been run
813 64
				$members = DdsMember::find()->where(['class_type'=>$classType])->all();
814 64
				$membersByRef = [];
815 64
				foreach ($members as $member)
816 64
					$membersByRef[$member['member_ref']] = $member->attributes;
0 ignored issues
show
Bug introduced by
Accessing attributes on the interface yii\db\ActiveRecordInterface suggest that you code against a concrete implementation. How about adding an instanceof check?
Loading history...
817 64
				$_classMembers[$classType] = $membersByRef;
818
			} catch (\Exception $e) {
819
				throw new \InvalidArgumentException("Error attempting to get members for $classType.");
820
			}
821
		}
822 66
		if (empty($dataTypes))
823 66
			return $_classMembers[$classType];
824 16
		$dataMembers = [];
825 16
		foreach ($_classMembers[$classType] as $k=>$m) {
826 16
			if (in_array($m['data_type_ref'], $dataTypes))
827 4
				$dataMembers[$k] = $m;
828
		}
829 16
		return $dataMembers;
830
	}
831
832
	/**
833
	 * get the object table name from the class type
834
	 * @param string $classType
835
	 * @return string
836
	 */
837 86
	protected function getTableFromClassType($classType)
838
	{
839 86
		return "ddt_".$this->canonicaliseRef($classType);
840
	}
841
842
	/**
843
	 * Get the basic create sql for a table
844
	 * @param string $tableName
845
	 * @return string
846
	 */
847 84
	protected function getCreateTableSql($tableName)
848
	{
849 84
		$tableOptions = null;
850 84
		if (neon()->db->driverName === 'mysql') {
851 84
			$tableOptions = self::MYSQL_DEFAULT_COLLATION.' ENGINE=MYISAM';
852
		}
853 84
		$uuidCollation = $this->getCollation('UUID');
854 84
		return "CREATE TABLE IF NOT EXISTS `$tableName` (`_uuid` CHAR(22) $uuidCollation NOT NULL COMMENT 'object _uuid from the dds_object table', PRIMARY KEY (`_uuid`)) $tableOptions;";
855
	}
856
857
	/**
858
	 * Get the basic drop table sql
859
	 * @param string $classType
860
	 * @return array
861
	 */
862 84
	protected function getDropTableSql($classType)
863
	{
864 84
		$classType = $this->canonicaliseRef($classType);
865 84
		$tableName = $this->getTableFromClassType($classType);
866
		return [
867 84
			"DELETE FROM `dds_object` WHERE `_class_type`='$classType';",
868 84
			"DROP TABLE IF EXISTS `$tableName`;"
869
		];
870
	}
871
872
	/**
873
	 * Converts an array of field=>values from a database row into a REPLACE SQL statement
874
	 * @param string $table
875
	 * @param string $row  the
876
	 * @return string
877
	 */
878 90
	protected function getTableRowReplaceSql($table, $row)
879
	{
880 90
		if (!is_array($row))
0 ignored issues
show
introduced by
The condition is_array($row) is always false.
Loading history...
881 90
			$row = $row->toArray();
882 90
		$fields = [];
883 90
		$values = [];
884 90
		foreach ($row as $f=>$v) {
885 90
			$fields[]=$f;
886 90
			$values[] = $this->pdoQuote($v);
887
		}
888 90
		if (count($fields))
889 90
			return "REPLACE INTO `$table` (`".(implode('`,`',$fields)).'`) VALUES ('.(implode(",",$values)).");";
890
		return null;
891
	}
892
893
	/**
894
	 * protect the values for PDO insertion
895
	 * @param mixed $value
896
	 * @return string
897
	 */
898 90
	private function pdoQuote($value)
899
	{
900 90
		if (is_array($value))
901 2
			$value = json_encode($value);
902 90
		if (is_null($value))
903 84
			return 'null';
904 90
		return neon()->db->pdo->quote($value);
905
	}
906
907
908
	/**
909
	 * generate a UUID
910
	 * @return string[22]  the uuid in base 64
911
	 */
912 42
	protected function generateUUID()
913
	{
914 42
		return Hash::uuid64();
915
	}
916
917
	/**
918
	 * test where or not an item is a UUID (in base 64)
919
	 *
920
	 * @param string $candidate
921
	 * @return boolean
922
	 */
923 66
	protected function isUUID($candidate)
924
	{
925 66
		return Hash::isUuid64($candidate);
926
	}
927
928
	/**
929
	 * test whether or not an array of items are all UUIDs
930
	 * (in base 64)
931
	 *
932
	 * @param array $candidates
933
	 * @return boolean
934
	 */
935 6
	protected function areUUIDs(array $candidates)
936
	{
937 6
		foreach ($candidates as $candidate) {
938 6
			if (!$this->isUUID($candidate))
939
				return false;
940
		}
941 6
		return true;
942
	}
943
}
944