CRUD   F
last analyzed

Complexity

Total Complexity 63

Size/Duplication

Total Lines 412
Duplicated Lines 0 %

Test Coverage

Coverage 98.54%

Importance

Changes 0
Metric Value
dl 0
loc 412
ccs 202
cts 205
cp 0.9854
rs 3.6585
c 0
b 0
f 0
wmc 63

13 Methods

Rating   Name   Duplication   Size   Complexity  
C read_internal() 0 36 8
C read_field_post_processing() 0 23 7
C create_internal() 0 42 7
C read_joined_table() 0 51 12
B update_internal() 0 34 4
C delete_internal() 0 28 8
A read() 0 8 1
A format_without_data_model() 0 2 1
A delete() 0 8 1
C update_joined_tables() 0 45 7
A create() 0 11 3
A update() 0 11 2
A format_with_data_model() 0 2 2

How to fix   Complexity   

Complex Class

Complex classes like CRUD 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 CRUD, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * @package CleverStyle Framework
4
 * @author  Nazar Mokrynskyi <[email protected]>
5
 * @license 0BSD
6
 */
7
namespace cs;
8
use
9
	cs\DB\Accessor,
10
	cs\CRUD\Data_model_processing;
11
12
/**
13
 * CRUD trait
14
 *
15
 * Provides create/read/update/delete methods for faster development
16
 */
17
trait CRUD {
18
	use
19
		Accessor,
20
		Data_model_processing;
21
	/**
22
	 * Create item
23
	 *
24
	 * @param array $arguments First element `id` can be omitted if it is autoincrement field
25
	 *
26
	 * @return false|int|string Id of created item on success, `false` otherwise
27
	 */
28 66
	protected function create (...$arguments) {
29 66
		if (count($arguments) == 1 && !is_array(array_values($this->data_model)[1])) {
30 54
			$arguments = $arguments[0];
31
		}
32
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
33 66
		$this->db_prime()->transaction(
34 66
			function () use (&$id, $arguments) {
35 66
				$id = $this->create_internal($this->table, $this->data_model, $arguments);
36 66
			}
37
		);
38 66
		return $id;
39
	}
40
	/**
41
	 * Create item
42
	 *
43
	 * @param string              $table
44
	 * @param callable[]|string[] $data_model
45
	 * @param array               $arguments First element `id` can be omitted if it is autoincrement field
46
	 *
47
	 * @return false|int|string Id of created item on success (or specified primary key), `false` otherwise
48
	 */
49 66
	private function create_internal ($table, $data_model, $arguments) {
50 66
		$arguments = $this->fix_arguments_order($data_model, $arguments);
51 66
		$insert_id = count($data_model) == count($arguments) ? $arguments[0] : false;
52 66
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(
53 66
			$insert_id !== false ? $data_model : array_slice($data_model, 1),
54 66
			$arguments,
55 66
			$insert_id,
56 66
			$update_needed
57
		);
58 66
		$columns = '`'.implode('`,`', array_keys($prepared_arguments)).'`';
59 66
		$values  = implode(',', array_fill(0, count($prepared_arguments), '?'));
60 66
		$return  = $this->db_prime()->q(
61 66
			"INSERT IGNORE INTO `$table`
62
				(
63 66
					$columns
64
				) VALUES (
65 66
					$values
66
				)",
67 66
			$prepared_arguments
68
		);
69 66
		$id      = $insert_id !== false ? $insert_id : $this->db_prime()->id();
70
		/**
71
		 * Id might be 0 if insertion failed or if we insert duplicate entry (which is fine since we use 'INSERT IGNORE'
72
		 */
73 66
		if (!$return || $id === 0) {
0 ignored issues
show
introduced by
The condition $return is always false.
Loading history...
74
			return false;
75
		}
76 66
		$this->update_joined_tables($id, $joined_tables);
77 66
		$this->find_update_files_tags($id, [], $arguments);
78
		/**
79
		 * If on creation request without specified primary key and multilingual fields present - update needed
80
		 * after creation (there is no id before creation)
81
		 */
82 66
		if ($update_needed) {
83 6
			$this->update_internal(
84 6
				$table,
85 6
				array_filter($data_model, [$this, 'format_without_data_model']),
86 6
				array_merge([array_keys($data_model)[0] => $id], $prepared_arguments),
87 6
				false
88
			);
89
		}
90 66
		return $id;
91
	}
92
	/**
93
	 * @param mixed $format
94
	 *
95
	 * @return bool
96
	 */
97 78
	private function format_without_data_model ($format) {
98 78
		return !$this->format_with_data_model($format);
99
	}
100
	/**
101
	 * @param mixed $format
102
	 *
103
	 * @return bool
104
	 */
105 78
	private function format_with_data_model ($format) {
106 78
		return is_array($format) && isset($format['data_model']);
107
	}
108
	/**
109
	 * @param int|string $id
110
	 * @param array      $joined_tables
111
	 */
112 72
	private function update_joined_tables ($id, $joined_tables) {
113 72
		$clang = $this->db_prime()->s(Language::instance()->clang, false);
114
		/**
115
		 * At first we remove all old data
116
		 */
117 72
		foreach ($this->data_model as $table => $model) {
118 72
			if ($this->format_without_data_model($model)) {
119 72
				continue;
120
			}
121 12
			$id_field                 = array_keys($model['data_model'])[0];
122 12
			$language_field_condition = isset($model['language_field'])
123 6
				? "AND `$model[language_field]` = '$clang'"
124 12
				: '';
125 12
			$this->db_prime()->q(
126 12
				"DELETE FROM `{$this->table}_$table`
127
				WHERE
128 12
					`$id_field`	= ?
129 12
					$language_field_condition",
130 12
				$id
0 ignored issues
show
Bug introduced by
$id of type integer|string is incompatible with the type array expected by parameter $parameters of cs\DB\_Abstract::q(). ( Ignorable by Annotation )

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

130
				/** @scrutinizer ignore-type */ $id
Loading history...
131
			);
132
		}
133 72
		$id = $this->db_prime()->s($id, false);
134
		/**
135
		 * Now walk through all tables and insert new valued
136
		 */
137 72
		foreach ($joined_tables as $table => $model) {
138 12
			if (!@$model['data']) {
139 9
				continue;
140
			}
141 12
			$fields = "`$model[id_field]`, ";
142 12
			$values = "'$id'";
143 12
			if (isset($model['language_field'])) {
144 6
				$fields .= "`$model[language_field]`, ";
145 6
				$values .= ",'$clang'";
146
			}
147 12
			$fields .= '`'.implode('`,`', array_keys($model['fields'])).'`';
148 12
			$values .= str_repeat(',?', count($model['fields']));
149 12
			$this->db_prime()->insert(
150 12
				"INSERT INTO `{$this->table}_$table`
151
					(
152 12
						$fields
153
					) VALUES (
154 12
						$values
155
					)",
156 12
				$model['data']
157
			);
158
		}
159 72
	}
160
	/**
161
	 * Read item
162
	 *
163
	 * @param int|int[]|string|string[] $id
164
	 *
165
	 * @return array|false
166
	 */
167 51
	protected function read ($id) {
168
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
169 51
		$this->db()->transaction(
170 51
			function () use (&$result, $id) {
171 51
				$result = $this->read_internal($this->table, $this->data_model, $id);
172 51
			}
173
		);
174 51
		return $result;
175
	}
176
	/**
177
	 * Read item
178
	 *
179
	 * @param string                    $table
180
	 * @param callable[]|string[]       $data_model
181
	 * @param int|int[]|string|string[] $id
182
	 *
183
	 * @return array|false
184
	 */
185 69
	private function read_internal ($table, $data_model, $id) {
186 69
		if (is_array($id)) {
187 12
			foreach ($id as &$i) {
188 12
				$i = $this->read_internal($table, $data_model, $i);
189
			}
190 12
			return $id;
191
		}
192 69
		$columns      = array_filter($data_model, [$this, 'format_without_data_model']);
193 69
		$columns      = '`'.implode('`,`', array_keys($columns)).'`';
194 69
		$first_column = array_keys($data_model)[0];
195 69
		$data         = $this->db()->qf(
196 69
			"SELECT $columns
0 ignored issues
show
Bug introduced by
'SELECT '.$columns.' ...lumn.'` = ? LIMIT 1' of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qf(). ( Ignorable by Annotation )

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

196
			/** @scrutinizer ignore-type */ "SELECT $columns
Loading history...
197 69
			FROM `$table`
198 69
			WHERE `$first_column` = ?
199
			LIMIT 1",
200 69
			$id
0 ignored issues
show
Bug introduced by
$id of type integer|string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qf(). ( Ignorable by Annotation )

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

200
			/** @scrutinizer ignore-type */ $id
Loading history...
201
		);
202 69
		if (!$data) {
0 ignored issues
show
introduced by
The condition $data is always false.
Loading history...
203 15
			return false;
204
		}
205 69
		foreach ($this->data_model as $field => $model) {
206 69
			if ($this->format_with_data_model($model)) {
207 9
				$data[$field] = $this->read_joined_table($id, $field, $model);
208
			} else {
209 69
				if (is_string($model)) {
210
					/**
211
					 * Handle multilingual fields automatically
212
					 */
213 69
					if (strpos($model, 'ml:') === 0) {
214 6
						$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
215
					}
216
				}
217 69
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
218
			}
219
		}
220 69
		return $data;
221
	}
222
	/**
223
	 * @param false|string|string[] $value
224
	 * @param callable|string       $model
225
	 *
226
	 * @return array|false|float|int|string
227
	 */
228 72
	private function read_field_post_processing ($value, $model) {
229 72
		if (is_array($value)) {
230 18
			foreach ($value as &$v) {
231 18
				$v = $this->read_field_post_processing($v, $model);
232
			}
233 18
			return $value;
234
		}
235 72
		if (is_callable($model)) {
236 9
			return $model($value);
237
		}
238
		/**
239
		 * Decode JSON fields
240
		 */
241 72
		if (in_array($model, ['json', 'ml:json'])) {
242 51
			return _json_decode($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type false; however, parameter $in of _json_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

242
			return _json_decode(/** @scrutinizer ignore-type */ $value);
Loading history...
243
		}
244 72
		if (strpos($model, 'int') === 0) {
0 ignored issues
show
Bug introduced by
It seems like $model can also be of type callable; however, parameter $haystack of strpos() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

244
		if (strpos(/** @scrutinizer ignore-type */ $model, 'int') === 0) {
Loading history...
245 60
			return (int)$value;
246
		}
247 69
		if (strpos($model, 'float') === 0) {
248 3
			return (float)$value;
249
		}
250 69
		return $value;
251
	}
252
	/**
253
	 * @param int|string  $id
254
	 * @param string      $table
255
	 * @param array       $model
256
	 * @param null|string $force_clang
257
	 *
258
	 * @return array
259
	 */
260 9
	private function read_joined_table ($id, $table, $model, $force_clang = null) {
261 9
		$clang                    = $force_clang ?: $this->db()->s(Language::instance()->clang, false);
262 9
		$id_field                 = array_keys($model['data_model'])[0];
263 9
		$language_field_condition = isset($model['language_field'])
264 6
			? "AND `$model[language_field]` = '$clang'"
265 9
			: '';
266 9
		$fields                   = '`'.implode('`,`', array_keys($model['data_model'])).'`';
267 9
		$rows                     = $this->db_prime()->qfa(
268 9
			"SELECT $fields
0 ignored issues
show
Bug introduced by
'SELECT '.$fields.' F...anguage_field_condition of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfa(). ( Ignorable by Annotation )

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

268
			/** @scrutinizer ignore-type */ "SELECT $fields
Loading history...
269 9
			FROM `{$this->table}_$table`
270
			WHERE
271 9
				`$id_field`	= ?
272 9
				$language_field_condition",
273 9
			$id
0 ignored issues
show
Bug introduced by
$id of type integer|string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfa(). ( Ignorable by Annotation )

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

273
			/** @scrutinizer ignore-type */ $id
Loading history...
274 9
		) ?: [];
275 9
		$language_field           = isset($model['language_field']) ? $model['language_field'] : null;
276
		/**
277
		 * If no rows found for current language - find another language that should contain some rows
278
		 */
279 9
		if (!$rows && $language_field !== null) {
280 6
			$new_clang = $this->db_prime()->qfs(
281 6
				"SELECT `$language_field`
0 ignored issues
show
Bug introduced by
'SELECT `'.$language_fie...eld.'` = ? LIMIT 1' of type string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfs(). ( Ignorable by Annotation )

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

281
				/** @scrutinizer ignore-type */ "SELECT `$language_field`
Loading history...
282 6
				FROM `{$this->table}_$table`
283 6
				WHERE `$id_field`	= ?
284
				LIMIT 1",
285 6
				$id
0 ignored issues
show
Bug introduced by
$id of type integer|string is incompatible with the type string[] expected by parameter $query of cs\DB\_Abstract::qfs(). ( Ignorable by Annotation )

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

285
				/** @scrutinizer ignore-type */ $id
Loading history...
286
			);
287 6
			if ($new_clang && $new_clang != $clang) {
288 6
				return $this->read_joined_table($id, $table, $model, $new_clang);
289
			}
290
			return [];
291
		}
292 9
		foreach ($rows as &$row) {
293
			/**
294
			 * Drop language and id field since they are used internally, not specified by user
295
			 */
296
			unset(
297 9
				$row[$language_field],
298 9
				$row[$id_field]
299
			);
300 9
			foreach ($row as $field => &$value) {
301 9
				$value = $this->read_field_post_processing($value, $model['data_model'][$field]);
302
			}
303
			/**
304
			 * If row is array that contains only one item - lets make resulting array flat
305
			 */
306 9
			if (count($row) == 1) {
307 9
				$row = array_pop($row);
308
			}
309
		}
310 9
		return $rows;
311
	}
312
	/**
313
	 * Update item
314
	 *
315
	 * @param array $arguments
316
	 *
317
	 * @return bool
318
	 */
319 27
	protected function update (...$arguments) {
320 27
		if (count($arguments) == 1) {
321 15
			$arguments = $arguments[0];
322
		}
323
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
324 27
		$this->db_prime()->transaction(
325 27
			function () use (&$result, $arguments) {
326 27
				$result = $this->update_internal($this->table, $this->data_model, $arguments);
327 27
			}
328
		);
329 27
		return $result;
330
	}
331
	/**
332
	 * Update item
333
	 *
334
	 * @param string              $table
335
	 * @param callable[]|string[] $data_model
336
	 * @param array               $arguments
337
	 * @param bool                $files_update
338
	 *
339
	 * @return bool
340
	 */
341 27
	private function update_internal ($table, $data_model, $arguments, $files_update = true) {
342 27
		$arguments          = $this->fix_arguments_order($data_model, $arguments);
343 27
		$prepared_arguments = $arguments;
344 27
		$id                 = array_shift($prepared_arguments);
345 27
		$data               = $this->read_internal($table, $data_model, $id);
346 27
		if (!$data) {
0 ignored issues
show
introduced by
The condition $data is always false.
Loading history...
347 3
			return false;
348
		}
349 27
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(array_slice($data_model, 1), $prepared_arguments, $id);
350 27
		$columns              = implode(
351 27
			',',
352 27
			array_map(
353 27
				function ($column) {
354 27
					return "`$column` = ?";
355 27
				},
356 27
				array_keys($prepared_arguments)
357
			)
358
		);
359 27
		$prepared_arguments[] = $id;
360 27
		$first_column         = array_keys($data_model)[0];
361 27
		if (!$this->db_prime()->q(
362 27
			"UPDATE `$table`
363 27
			SET $columns
364 27
			WHERE `$first_column` = ?",
365 27
			$prepared_arguments
366
		)
367
		) {
368
			return false;
369
		}
370 27
		if ($files_update) {
371 27
			$this->update_joined_tables($id, $joined_tables);
372 27
			$this->find_update_files_tags($id, $data, $arguments);
373
		}
374 27
		return true;
375
	}
376
	/**
377
	 * Delete item
378
	 *
379
	 * @param int|int[]|string|string[] $id
380
	 *
381
	 * @return bool
382
	 */
383 30
	protected function delete ($id) {
384
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
385 30
		$this->db_prime()->transaction(
386 30
			function () use (&$result, $id) {
387 30
				$result = $this->delete_internal($this->table, $this->data_model, $id);
388 30
			}
389
		);
390 30
		return $result;
391
	}
392
	/**
393
	 * Delete item
394
	 *
395
	 * @param string                    $table
396
	 * @param callable[]|string[]       $data_model
397
	 * @param int|int[]|string|string[] $id
398
	 *
399
	 * @return bool
400
	 */
401 30
	private function delete_internal ($table, $data_model, $id) {
402 30
		$id           = (array)$id;
403 30
		$result       = true;
404 30
		$multilingual = $this->is_multilingual();
405 30
		$first_column = array_keys($data_model)[0];
406 30
		foreach ($id as $i) {
407
			$result =
408 30
				$result &&
409 30
				$this->read_internal($table, $data_model, $i) &&
410 30
				$this->db_prime()->q(
411 30
					"DELETE FROM `$table`
412 30
					WHERE `$first_column` = ?",
413 30
					$i
414
				);
415
			/**
416
			 * If there are multilingual fields - handle multilingual deleting of fields automatically
417
			 */
418 30
			if ($multilingual) {
419 3
				foreach (array_keys($this->data_model) as $field) {
420 3
					if (is_string($this->data_model[$field]) && strpos($this->data_model[$field], 'ml:') === 0) {
421 3
						Text::instance()->del($this->cdb(), "$this->data_model_ml_group/$field", $i);
422
					}
423
				}
424
			}
425 30
			$this->update_joined_tables($i, []);
426 30
			$this->delete_files_tags($i);
427
		}
428 30
		return $result;
429
	}
430
}
431