Completed
Push — master ( 5b91fe...d7aa39 )
by Nazar
07:05
created

CRUD::update_internal()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 34
Code Lines 25

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 26
CRAP Score 4.0008

Importance

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

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

197
			/** @scrutinizer ignore-type */ "SELECT $columns
Loading history...
198 69
			FROM `$table`
199 69
			WHERE `$first_column` = ?
200
			LIMIT 1",
201 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

201
			/** @scrutinizer ignore-type */ $id
Loading history...
202
		);
203 69
		if (!$data) {
0 ignored issues
show
introduced by
The condition ! $data can never be false.
Loading history...
204 15
			return false;
205
		}
206 69
		foreach ($this->data_model as $field => $model) {
207 69
			if ($this->format_with_data_model($model)) {
208 9
				$data[$field] = $this->read_joined_table($id, $field, $model);
209
			} else {
210 69
				if (is_string($model)) {
211
					/**
212
					 * Handle multilingual fields automatically
213
					 */
214 69
					if (strpos($model, 'ml:') === 0) {
215 6
						$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
216
					}
217
				}
218 69
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
219
			}
220
		}
221 69
		return $data;
222
	}
223
	/**
224
	 * @param false|string|string[] $value
225
	 * @param callable|string       $model
226
	 *
227
	 * @return array|false|float|int|string
228
	 */
229 72
	private function read_field_post_processing ($value, $model) {
230 72
		if (is_array($value)) {
231 18
			foreach ($value as &$v) {
232 18
				$v = $this->read_field_post_processing($v, $model);
233
			}
234 18
			return $value;
235
		}
236 72
		if (is_callable($model)) {
237 9
			return $model($value);
238
		}
239
		/**
240
		 * Decode JSON fields
241
		 */
242 72
		if (in_array($model, ['json', 'ml:json'])) {
243 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

243
			return _json_decode(/** @scrutinizer ignore-type */ $value);
Loading history...
244
		}
245 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

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

269
			/** @scrutinizer ignore-type */ "SELECT $fields
Loading history...
270 9
			FROM `{$this->table}_$table`
271
			WHERE
272 9
				`$id_field`	= ?
273 9
				$language_field_condition",
274 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

274
			/** @scrutinizer ignore-type */ $id
Loading history...
275 9
		) ?: [];
276 9
		$language_field           = isset($model['language_field']) ? $model['language_field'] : null;
277
		/**
278
		 * If no rows found for current language - find another language that should contain some rows
279
		 */
280 9
		if (!$rows && $language_field !== null) {
281 6
			$new_clang = $this->db_prime()->qfs(
282 6
				"SELECT `$language_field`
0 ignored issues
show
Bug introduced by
EncapsedNode 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

282
				/** @scrutinizer ignore-type */ "SELECT `$language_field`
Loading history...
283 6
				FROM `{$this->table}_$table`
284 6
				WHERE `$id_field`	= ?
285
				LIMIT 1",
286 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

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