DdsCore   F
last analyzed

Complexity

Total Complexity 199

Size/Duplication

Total Lines 928
Duplicated Lines 0 %

Test Coverage

Coverage 86.71%

Importance

Changes 2
Bugs 0 Features 1
Metric Value
eloc 394
dl 0
loc 928
ccs 372
cts 429
cp 0.8671
rs 2
c 2
b 0
f 1
wmc 199

46 Methods

Rating   Name   Duplication   Size   Complexity  
A findMember() 0 4 1
A canonicalise() 0 3 1
A quoteField() 0 6 2
A canonicaliseRefs() 0 7 2
A findClass() 0 10 4
A hasObjects() 0 4 1
C canonicaliseFilter() 0 23 17
A canonicaliseRef() 0 3 1
A findDataType() 0 4 1
A setClassMapMemberCache() 0 3 1
A createClassTable() 0 8 1
A storeMigration() 0 3 1
A clearClassCache() 0 4 1
A listStorageTypes() 0 3 1
A dropClassMemberColumn() 0 18 3
A getCollation() 0 9 2
A canonicaliseRefByParts() 0 7 2
A getMapMemberForClass() 0 11 3
A clearClassMemberCache() 0 4 1
A now() 0 3 1
A removeMigration() 0 3 1
B canonicaliseFiltersRecursive() 0 24 9
A dropClassTable() 0 8 2
B addClassMemberColumn() 0 25 6
A getMemberStorage() 0 26 4
A operatorTakesObject() 0 9 3
D getIndexType() 0 28 18
B listMembersForClass() 0 30 9
A checkLogic() 0 20 4
A canonicaliseFilters() 0 10 3
D getColumnType() 0 27 19
A areUUIDs() 0 7 3
A convertFromPHPToDB() 0 10 4
B canonicaliseLimit() 0 25 8
B getClassMembers() 0 24 7
A generateUUID() 0 3 1
B canonicaliseOrder() 0 27 9
C doConversionFromPHPToDB() 0 30 12
A pdoQuote() 0 7 3
A getTableFromClassType() 0 3 1
A getCreateTableSql() 0 8 2
C doConversionFromDBToPHP() 0 32 14
A convertFromDBToPHP() 0 9 4
A getDropTableSql() 0 7 1
A isUUID() 0 3 1
A getTableRowReplaceSql() 0 13 4

How to fix   Complexity   

Complex Class

Complex classes like DdsCore often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use DdsCore, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @link http://www.newicon.net/neon
4
 * @copyright Copyright (c) 2016 Newicon Ltd
5
 * @license http://www.newicon.net/neon/license/
6
 */
7
8
namespace neon\daedalus\services\ddsManager;
9
10
use 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 74
	public function listStorageTypes()
43
	{
44 74
		return DdsStorage::find()->asArray()->all();
45
	}
46
47
	/**
48
	 * @inheritdoc
49
	 */
50 86
	public function now()
51
	{
52 86
		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 94
	protected function storeMigration($up, $down)
66
	{
67 94
		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 88
	protected function createClassTable($classType)
85
	{
86 88
		$tableName = $this->getTableFromClassType($classType);
87
		// create the up and down sql migration code
88 88
		$upSql = $this->getCreateTableSql($tableName);
89 88
		$downSql = $this->getDropTableSql($classType);
90 88
		neon()->db->createCommand($upSql)->execute();
91 88
		$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 88
	}
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 86
	protected function addClassMemberColumn($classType, $memberRef)
109
	{
110 86
		$storage = $this->getMemberStorage($classType, $memberRef);
111 86
		$table = $this->getTableFromClassType($classType);
112 86
		$member = $storage[$memberRef];
113
		// handle some storage differences
114 86
		$columnCheck = substr($member['column'], 0, 4);
115 86
		if ($columnCheck=='CHAR' || $columnCheck=='UUID') {
116 20
			$size = (isset($member['definition']['size'])) ? $member['definition']['size'] : 150;
117 20
			$member['column'] = str_replace($columnCheck, "CHAR($size)", $member['column']);
118
		}
119
		try {
120 86
			$upMember = "ALTER TABLE `$table` ADD `$memberRef` $member[column];";
121 86
			$downMember = "ALTER TABLE `$table` DROP `$memberRef`;";
122 86
			neon()->db->createCommand($upMember)->execute();
123 86
			$this->storeMigration($upMember, $downMember);
124 86
			if ($member['index']) {
125 86
				$upIndex = "ALTER TABLE `$table` ADD ".$member['index']."(`$memberRef`);";
126 86
				$downIndex = "ALTER TABLE `$table` DROP INDEX `$memberRef`;";
127 86
				neon()->db->createCommand($upIndex)->execute();
128 86
				$this->storeMigration($upIndex, $downIndex);
129
			}
130 86
			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 88
	protected function getMemberStorage($classType, $memberRef=null)
165
	{
166 88
		$boundValues = [];
167
		$query =<<<EOQ
168 88
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 88
		$boundValues[':classType'] = $classType;
174 88
		if ($memberRef) {
175 86
			$query .= ' AND `m`.`member_ref`=:memberRef';
176 86
			$boundValues[':memberRef'] = $memberRef;
177
		}
178
179 88
		$cmd = neon()->db->createCommand($query, $boundValues);
180 88
		$rows = $cmd->queryAll();
181 88
		$memberStorage = [];
182 88
		foreach ($rows as $r) {
183 88
			$memberStorage[$r['member_ref']] = [
184 88
				'column' => $this->getColumnType($r['type']),
185 88
				'index' => $this->getIndexType($r['type']),
186 88
				'definition' => empty($r['definition']) ? null : json_decode($r['definition'],true)
187
			];
188
		}
189 88
		return $memberStorage;
190
	}
191
192 88
	protected function getColumnType($storageType)
193
	{
194 88
		$type = '';
195 88
		switch (strtoupper($storageType)) {
196 88
			case 'INTEGER_TINY': $type = 'TINYINT'; break;
197 88
			case 'INTEGER_SHORT': $type = 'SMALLINT'; break;
198 88
			case 'INTEGER': $type = 'INT'; break;
199 88
			case 'INTEGER_LONG': $type = 'BIGINT'; break;
200 88
			case 'FLOAT': $type = 'FLOAT'; break;
201 88
			case 'DOUBLE': $type = 'DOUBLE'; break;
202 88
			case 'DATE': $type = 'DATE'; break;
203 88
			case 'DATETIME': $type = 'DATETIME'; break;
204 88
			case 'TIME': $type = 'TIME'; break;
205 88
			case 'TEXT_SHORT': $type = 'VARCHAR(150)'; break;
206 24
			case 'TEXT': $type = 'TEXT'; break;
207 24
			case 'TEXT_LONG': $type = 'MEDIUMTEXT'; break;
208 22
			case 'BINARY_SHORT': $type = 'BLOB'; break;
209 22
			case 'BINARY': $type = 'MEDIUMBLOB'; break;
210 22
			case 'BINARY_LONG': $type = 'LONGBLOB'; break;
211 22
			case 'CHAR': $type = 'CHAR'; break;
212 12
			case 'UUID': $type = 'UUID'; break;
213
			default: $type="UNKNOWN STORAGE TYPE $storageType"; break;
214
		}
215 88
		$collation = $this->getCollation($storageType);
216 88
		if ($collation)
217 12
			return "$type $collation DEFAULT NULL ";
218 86
		return "$type DEFAULT NULL ";
219
	}
220
221 88
	protected function getIndexType($storageType)
222
	{
223 88
		$index = '';
224 88
		switch (strtoupper($storageType)) {
225 88
			case 'INTEGER_TINY':
226 88
			case 'INTEGER_SHORT':
227 88
			case 'INTEGER':
228 88
			case 'INTEGER_LONG':
229 88
			case 'FLOAT':
230 88
			case 'DOUBLE':
231 88
			case 'DATE':
232 88
			case 'DATETIME':
233 88
			case 'TIME':
234 88
			case 'TEXT_SHORT':
235 24
			case 'CHAR':
236 16
			case 'UUID':
237 88
				$index = 'INDEX';
238 88
			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 88
		return $index;
249
	}
250
251 90
	protected function getCollation($storageType)
252
	{
253 90
		$collation = null;
254
		switch ($storageType) {
255 90
			case 'UUID':
256 88
				$collation = 'CHARACTER SET latin1 COLLATE latin1_general_cs';
257 88
			break;
258
		}
259 90
		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 90
	protected function findClass($classType, &$class=null, $throwException=false)
289
	{
290 90
		$ct = $this->canonicaliseRef($classType);
291 90
		if (empty(self::$_classCache[$ct])) {
292 90
			self::$_classCache[$ct] = DdsClass::findOne(['class_type' => $ct]);
293
		}
294 90
		$class = self::$_classCache[$ct];
295 90
		if (!$class && $throwException)
296
			throw new InvalidArgumentException('Unknown class type "'.$ct.'"');
297 90
		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 88
	protected function clearClassMemberCache($classType)
316
	{
317 88
		unset(static::$_classMembersCache[$classType]);
318 88
		unset(static::$_classMemberMapCache[$classType]);
319 88
	}
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 88
	protected function findDataType($dataTypeRef, &$dataType=null)
404
	{
405 88
		$dataType = DdsDataType::findOne(['data_type_ref'=>$dataTypeRef]);
406 88
		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 102
	protected function canonicaliseRef($ref)
430
	{
431 102
		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 26
	protected function quoteField($ref, $ddsObjectAlias='o')
472
	{
473 26
		if (in_array($ref, ['_uuid', '_created', '_updated', '_class_ref'])) {
474 18
			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 string $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 28
	protected function canonicaliseOrder($order)
625
	{
626 28
		$canon = [];
627 28
		if (is_array($order)) {
0 ignored issues
show
introduced by
The condition is_array($order) is always true.
Loading history...
628 28
			foreach ($order as $k=>$d) {
629 20
				$drn = strtoupper($d);
630 20
				switch ($drn) {
631 20
					case 'ASC': case 'DESC':
632
						// allow -ve key starts for nulls last in MySql
633 20
						if (strpos($k,'-') === 0)
634 2
							$canon['-'.$this->quoteField($k)] = $drn;
635
						else
636 20
							$canon[$this->quoteField($k)] = $drn;
637 20
					break;
638
					case 'ASC_L':
639
						$canon[$k] = 'ASC';
640
					break;
641
					case 'DESC_L':
642
						$canon[$k] = 'DESC';
643
					break;
644
					case 'RAND':
645
						$canon['RAND'] = 'RAND';
646
					break;
647
				}
648
			}
649
		}
650 28
		return $canon;
651
	}
652
653
	/**
654
	 * canonicalise the requested limit
655
	 * @param array $limit  the uncanonicalised limit
656
	 * @param int &$total  the total value extracted from the limit
657
	 * @param bool &$calculateTotal  whether or not to calculate the total
658
	 *   This is true if $total is set to true.
659
	 * @return array  the canonicalised limit
660
	 */
661 16
	protected function canonicaliseLimit($limit, &$total, &$calculateTotal)
662
	{
663 16
		$canon = [];
664 16
		$total=null;
665 16
		$calculateTotal = false;
666 16
		if (is_array($limit)) {
0 ignored issues
show
introduced by
The condition is_array($limit) is always true.
Loading history...
667 16
			$canon = ['start'=>0,'length'=>self::MAX_LENGTH];
668 16
			foreach ($limit as $k=>$v) {
669 4
				$key = strtolower($k);
670 4
				switch ($key) {
671 4
					case 'start':
672 4
						$canon[$key] = (int) $v;
673 4
					break;
674 4
					case 'length':
675 4
						$canon[$key] = min((int) $v, self::MAX_LENGTH);
676 4
					break;
677 2
					case 'total':
678
						// $v can be truthy or the previous integer
679 2
						$total = is_numeric($v) ? (int) $v : null;
680 2
						$calculateTotal = ($v===true || $v==='true');
681 2
					break;
682
				}
683
			}
684
		}
685 16
		return $canon;
686
	}
687
688
	/**
689
	 * Convert row from the database to the formats required by PHP
690
	 * e.g. booleans from 1/0 to true/false
691
	 * @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...
692
	 * @param string $classTypeKey the key in the data that will give the class type
693
	 */
694 36
	protected function convertFromDBToPHP(&$row, $links=[], $classTypeKey='_class_type')
695
	{
696 36
		$classType = $row[$classTypeKey];
697 36
		$members = $this->getClassMembers($classType);
698
		// now process only the member defined fields:
699 36
		foreach ($row as $key => &$value) {
700 36
			if (isset($members[$key])) {
701 36
				$memberLinks = isset($links[$key])?$links[$key]:[];
702 36
				$this->doConversionFromDBToPHP($members[$key], $value, $memberLinks);
703
			}
704
		}
705 36
	}
706
707
	/**
708
	 * The actual converter from DB to PHP. Override this if the data type is not
709
	 * handled here, and call this if not handled in the overridden method
710
	 * @param string $dataType the data type ref of the value
711
	 * @param mixed $value  the value returned by the database
712
	 */
713 36
	protected function doConversionFromDBToPHP($member, &$value, $memberLinks=[])
714
	{
715 36
		switch($member['data_type_ref']) {
716 36
			case 'choice':
717
				// silently ignore deleted old choice as no longer valid
718
				if (is_array($member['choices']) && isset($member['choices'][$value])) {
719
					$value = ['key'=>$value, 'value'=>$member['choices'][$value], 'type'=>'choice'];
720
				}
721
			break;
722 36
			case 'choice_multiple':
723
				$choices = json_decode($value, true);
724
				$value = [];
725
726
				// protect against non array values
727
				if (!empty($choices) && is_array($choices)) {
728
					foreach ($choices as $choice) {
729
						// silently ignore deleted old choice as no longer valid
730
						if (isset($member['choices'][$choice]))
731
							$value[] = ['key'=>$choice, 'value'=>$member['choices'][$choice]];
732
					}
733
				}
734
			break;
735 36
			case 'boolean':
736 2
				if ($value === NULL)
737
					return;
738 2
				$value = !!$value;
739 2
			break;
740 36
			case 'json': $value = json_decode($value, true); break;
741 36
			case 'link_multi':
742 36
			case 'file_ref_multi':
743 6
				$value = $memberLinks;
744 6
			break;
745
		}
746 36
	}
747
748
	/**
749
	 * convert data from PHP format to the database format
750
	 * @param string $classType
751
	 * @param array $data  the object data
752
	 * @param array &$links  on return, any links are extracted into this
753
	 */
754 70
	protected function convertFromPHPToDB($classType, &$data, &$links)
755
	{
756 70
		$links = [];
757 70
		$members = $this->getClassMembers($classType);
758 70
		foreach ($data as $key=>&$value) {
759 70
			$itemLinks = null;
760 70
			if (isset($members[$key])) {
761 70
				$this->doConversionFromPHPToDB($members[$key], $value, $itemLinks);
762 70
				if ($itemLinks !== null)
763 8
					$links[$key] = $itemLinks;
764
			}
765
		}
766 70
	}
767
768
	/**
769
	 * The actual converter from PHP to DB. Override this if the data type is not
770
	 * handled here, and call this if not handled in the overridden method
771
	 *
772
	 * @param array $member
773
	 * @param mixed $value
774
	 */
775 70
	protected function doConversionFromPHPToDB($member, &$value, &$links)
776
	{
777 70
		$links = null;
778 70
		switch($member['data_type_ref']) {
779 70
			case 'choice':
780
				// convert from the value array to the key if the array
781
				// the array was returned
782
				if (is_array($value) && isset($value['key']))
783
					$value = $value['key'];
784
			break;
785 70
			case 'choice_multiple':
786
				$value = json_encode($value);
787
			break;
788 70
			case 'boolean':
789
				// check for null values
790 2
				if ($value === null)
791
					return;
792
				// convert from truthy to database 1 or 0
793 2
				$value = $value ? 1 : 0;
794 2
			break;
795 70
			case 'json';
796
				// json encode the PHP object / array
797 2
				$value = json_encode($value);
798 2
			break;
799 70
			case 'link_multi':
800 66
			case 'file_ref_multi':
801
				// extract out the links so they can be saved separately
802 8
				$links = empty($value) ? [] : $value;
803 8
				$value = null;
804 8
			break;
805
		}
806 70
	}
807
808
	/**
809
	 * get hold of all of the class members given a classType
810
	 * @param string $classType
811
	 * @param array $dataTypes  add to restrict members to certain types
812
	 * @return array the members
813
	 */
814 70
	protected function getClassMembers($classType, array $dataTypes=[])
815
	{
816 70
		static $_classMembers = [];
817 70
		if (!array_key_exists($classType, $_classMembers)) {
818
			try {
819
				// get the members and convert to array below to
820
				// make sure model afterFind has been run
821 68
				$members = DdsMember::find()->where(['class_type'=>$classType])->all();
822 68
				$membersByRef = [];
823 68
				foreach ($members as $member)
824 68
					$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...
825 68
				$_classMembers[$classType] = $membersByRef;
826
			} catch (Exception $e) {
827
				throw new InvalidArgumentException("Error attempting to get members for $classType.");
828
			}
829
		}
830 70
		if (empty($dataTypes))
831 70
			return $_classMembers[$classType];
832 16
		$dataMembers = [];
833 16
		foreach ($_classMembers[$classType] as $k=>$m) {
834 16
			if (in_array($m['data_type_ref'], $dataTypes))
835 4
				$dataMembers[$k] = $m;
836
		}
837 16
		return $dataMembers;
838
	}
839
840
	/**
841
	 * get the object table name from the class type
842
	 * @param string $classType
843
	 * @return string
844
	 */
845 90
	protected function getTableFromClassType($classType)
846
	{
847 90
		return 'ddt_' .$this->canonicaliseRef($classType);
848
	}
849
850
	/**
851
	 * Get the basic create sql for a table
852
	 * @param string $tableName
853
	 * @return string
854
	 */
855 88
	protected function getCreateTableSql($tableName)
856
	{
857 88
		$tableOptions = null;
858 88
		if (neon()->db->driverName === 'mysql') {
859 88
			$tableOptions = self::MYSQL_DEFAULT_COLLATION.' ENGINE=MYISAM';
860
		}
861 88
		$uuidCollation = $this->getCollation('UUID');
862 88
		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;";
863
	}
864
865
	/**
866
	 * Get the basic drop table sql
867
	 * @param string $classType
868
	 * @return array
869
	 */
870 88
	protected function getDropTableSql($classType)
871
	{
872 88
		$classType = $this->canonicaliseRef($classType);
873 88
		$tableName = $this->getTableFromClassType($classType);
874
		return [
875 88
			"DELETE FROM `dds_object` WHERE `_class_type`='$classType';",
876 88
			"DROP TABLE IF EXISTS `$tableName`;"
877
		];
878
	}
879
880
	/**
881
	 * Converts an array of field=>values from a database row into a REPLACE SQL statement
882
	 * @param string $table
883
	 * @param object|array $row  the table row
884
	 * @return string
885
	 */
886 94
	protected function getTableRowReplaceSql($table, $row)
887
	{
888 94
		if (!is_array($row))
889 94
			$row = $row->toArray();
890 94
		$fields = [];
891 94
		$values = [];
892 94
		foreach ($row as $f=>$v) {
893 94
			$fields[]=$f;
894 94
			$values[] = $this->pdoQuote($v);
895
		}
896 94
		if (count($fields))
897 94
			return "REPLACE INTO `$table` (`".(implode('`,`',$fields)).'`) VALUES ('.(implode(',', $values)). ');';
898
		return null;
899
	}
900
901
	/**
902
	 * protect the values for PDO insertion
903
	 * @param mixed $value
904
	 * @return string
905
	 */
906 94
	private function pdoQuote($value)
907
	{
908 94
		if (is_array($value))
909 2
			$value = json_encode($value);
910 94
		if (is_null($value))
911 88
			return 'null';
912 94
		return neon()->db->pdo->quote($value);
913
	}
914
915
916
	/**
917
	 * generate a UUID
918
	 * @return string[22]  the uuid in base 64
919
	 */
920 44
	protected function generateUUID()
921
	{
922 44
		return Hash::uuid64();
923
	}
924
925
	/**
926
	 * test where or not an item is a UUID (in base 64)
927
	 *
928
	 * @param string $candidate
929
	 * @return bool
930
	 */
931 70
	protected function isUUID($candidate)
932
	{
933 70
		return Hash::isUuid64($candidate);
934
	}
935
936
	/**
937
	 * test whether or not an array of items are all UUIDs
938
	 * (in base 64)
939
	 *
940
	 * @param array $candidates
941
	 * @return bool
942
	 */
943 8
	protected function areUUIDs(array $candidates)
944
	{
945 8
		foreach ($candidates as $candidate) {
946 8
			if (!$this->isUUID($candidate))
947
				return false;
948
		}
949 8
		return true;
950
	}
951
}
952