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); |
|
|
|
|
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); |
|
|
|
|
78
|
|
|
} |
79
|
|
|
|
80
|
|
|
/** |
81
|
|
|
* create a table for a particular class type |
82
|
|
|
* @param type $classType |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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'] |
|
|
|
|
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)); |
|
|
|
|
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 |
|
|
|
|
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)) |
|
|
|
|
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)); |
|
|
|
|
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) |
|
|
|
|
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)); |
|
|
|
|
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; |
|
|
|
|
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)) { |
|
|
|
|
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)) { |
|
|
|
|
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 |
|
|
|
|
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; |
|
|
|
|
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
|
|
|
|
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.