Passed
Push — develop ( 03421b...c9efed )
by steve
39:12 queued 25:46
created

DdsCore::canonicaliseOrder()   B

Complexity

Conditions 7
Paths 2

Size

Total Lines 21
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 7.392

Importance

Changes 0
Metric Value
eloc 15
dl 0
loc 21
rs 8.8333
c 0
b 0
f 0
cc 7
nc 2
nop 1
ccs 12
cts 15
cp 0.8
crap 7.392
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 Exception;
11
use InvalidArgumentException;
12
use neon\core\helpers\Hash;
13
use neon\daedalus\interfaces\IDdsBase;
14
use neon\daedalus\services\ddsManager\models\DdsClass;
15
use neon\daedalus\services\ddsManager\models\DdsDataType;
16
use neon\daedalus\services\ddsManager\models\DdsMember;
17
use neon\daedalus\services\ddsManager\models\DdsObject;
18
use neon\daedalus\services\ddsManager\models\DdsStorage;
19
use yii\base\Component;
20
21
22
class DdsCore extends Component implements IDdsBase
23
{
24
	public const MAX_LENGTH = 1000;
25
	public 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 DdsClass &$class
284
	 * @param bool $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 bool  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 DdsMember &$member
326
	 * @return bool  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 DdsDataType &$dataType
401
	 * @return bool  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 bool
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 int $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(
585
					'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.'
586
				);
587
			}
588
		}
589
		// test is to remove all keys and allowed characters and see if anything is
590
		// left over. If so then there must be bad characters ... one assumes
591 8
		$subLogic = str_replace($keys, '', $logic);
592 8
		$subLogic = str_replace(
593 8
			['AND', 'and', 'NOT', 'not', 'OR', 'or', ' ', ')', '('],
594 8
			'', $subLogic
595
		);
596 8
		if (strlen($subLogic)>0)
597 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($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

597
			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

597
			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...
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

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