Completed
Push — master ( 195cac...2c4aac )
by Nazar
05:02
created

CRUD::create_internal()   C

Complexity

Conditions 7
Paths 12

Size

Total Lines 43
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 21
CRAP Score 7.0046

Importance

Changes 0
Metric Value
cc 7
eloc 27
nc 12
nop 3
dl 0
loc 43
ccs 21
cts 22
cp 0.9545
crap 7.0046
rs 6.7272
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 44
	protected function create (...$arguments) {
30 44
		if (count($arguments) == 1 && !is_array(array_values($this->data_model)[1])) {
31 36
			$arguments = $arguments[0];
32
		}
33
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
34 44
		$this->db_prime()->transaction(
35
			function () use (&$id, $arguments) {
36 44
				$id = $this->create_internal($this->table, $this->data_model, $arguments);
37 44
			}
38
		);
39 44
		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 44
	private function create_internal ($table, $data_model, $arguments) {
51 44
		$arguments = $this->fix_arguments_order($data_model, $arguments);
52 44
		$insert_id = count($data_model) == count($arguments) ? $arguments[0] : false;
53 44
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(
54 44
			$insert_id !== false ? $data_model : array_slice($data_model, 1),
55
			$arguments,
56
			$insert_id,
57
			$update_needed
58
		);
59 44
		$columns = '`'.implode('`,`', array_keys($prepared_arguments)).'`';
60 44
		$values  = implode(',', array_fill(0, count($prepared_arguments), '?'));
61 44
		$return  = $this->db_prime()->q(
62 44
			"INSERT IGNORE INTO `$table`
63
				(
64 44
					$columns
65
				) VALUES (
66 44
					$values
67
				)",
68
			$prepared_arguments
69
		);
70 44
		$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 44
		if (!$return || $id === 0) {
75
			return false;
76
		}
77 44
		$this->update_joined_tables($id, $joined_tables);
78 44
		$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 44
		if ($update_needed) {
84 4
			$this->update_internal(
85
				$table,
86 4
				array_filter($data_model, [$this, 'format_without_data_model']),
87 4
				array_merge([array_keys($data_model)[0] => $id], $prepared_arguments),
88 4
				false
89
			);
90
		}
91 44
		return $id;
92
	}
93
	/**
94
	 * @param mixed $format
95
	 *
96
	 * @return bool
97
	 */
98 52
	private function format_without_data_model ($format) {
99 52
		return !$this->format_with_data_model($format);
100
	}
101
	/**
102
	 * @param mixed $format
103
	 *
104
	 * @return bool
105
	 */
106 52
	private function format_with_data_model ($format) {
107 52
		return is_array($format) && isset($format['data_model']);
108
	}
109
	/**
110
	 * @param int|string $id
111
	 * @param array      $joined_tables
112
	 */
113 48
	private function update_joined_tables ($id, $joined_tables) {
114 48
		$clang = $this->db_prime()->s(Language::instance()->clang, false);
115
		/**
116
		 * At first we remove all old data
117
		 */
118 48
		foreach ($this->data_model as $table => $model) {
119 48
			if ($this->format_without_data_model($model)) {
120 48
				continue;
121
			}
122 8
			$id_field                 = array_keys($model['data_model'])[0];
123 8
			$language_field_condition = isset($model['language_field'])
124 4
				? "AND `$model[language_field]` = '$clang'"
125 8
				: '';
126 8
			$this->db_prime()->q(
127 8
				"DELETE FROM `{$this->table}_$table`
128
				WHERE
129 8
					`$id_field`	= ?
130 8
					$language_field_condition",
131
				$id
132
			);
133
		}
134 48
		$id = $this->db_prime()->s($id, false);
135
		/**
136
		 * Now walk through all tables and insert new valued
137
		 */
138 48
		foreach ($joined_tables as $table => $model) {
139 8
			if (!@$model['data']) {
140 6
				continue;
141
			}
142 8
			$fields = "`$model[id_field]`, ";
143 8
			$values = "'$id'";
144 8
			if (isset($model['language_field'])) {
145 4
				$fields .= "`$model[language_field]`, ";
146 4
				$values .= ",'$clang'";
147
			}
148 8
			$fields .= '`'.implode('`,`', array_keys($model['fields'])).'`';
149 8
			$values .= str_repeat(',?', count($model['fields']));
150 8
			$this->db_prime()->insert(
151 8
				"INSERT INTO `{$this->table}_$table`
152
					(
153 8
						$fields
154
					) VALUES (
155 8
						$values
156
					)",
157 8
				$model['data']
158
			);
159
		}
160 48
	}
161
	/**
162
	 * Read item
163
	 *
164
	 * @param int|int[]|string|string[] $id
165
	 *
166
	 * @return array|false
167
	 */
168 34
	protected function read ($id) {
169
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
170 34
		$this->db()->transaction(
171
			function () use (&$result, $id) {
172 34
				$result = $this->read_internal($this->table, $this->data_model, $id);
173 34
			}
174
		);
175 34
		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 46
	private function read_internal ($table, $data_model, $id) {
187 46
		if (is_array($id)) {
188 8
			foreach ($id as &$i) {
189 8
				$i = $this->read_internal($table, $data_model, $i);
190
			}
191 8
			return $id;
192
		}
193 46
		$columns      = array_filter($data_model, [$this, 'format_without_data_model']);
194 46
		$columns      = '`'.implode('`,`', array_keys($columns)).'`';
195 46
		$first_column = array_keys($data_model)[0];
196 46
		$data         = $this->db()->qf(
197 46
			"SELECT $columns
198 46
			FROM `$table`
199 46
			WHERE `$first_column` = ?
200
			LIMIT 1",
201
			$id
202
		);
203 46
		if (!$data) {
204 10
			return false;
205
		}
206 46
		foreach ($this->data_model as $field => $model) {
207 46
			if ($this->format_with_data_model($model)) {
208 6
				$data[$field] = $this->read_joined_table($id, $field, $model);
209
			} else {
210 46
				if (is_string($model)) {
211
					/**
212
					 * Handle multilingual fields automatically
213
					 */
214 46
					if (strpos($model, 'ml:') === 0) {
215 4
						$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
216
					}
217
				}
218 46
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
219
			}
220
		}
221 46
		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 48
	private function read_field_post_processing ($value, $model) {
230 48
		if (is_array($value)) {
231 12
			foreach ($value as &$v) {
232 12
				$v = $this->read_field_post_processing($v, $model);
233
			}
234 12
			return $value;
235
		}
236 48
		if (is_callable($model)) {
237 6
			return $model($value);
238
		}
239
		/**
240
		 * Decode JSON fields
241
		 */
242 48
		if (in_array($model, ['json', 'ml:json'])) {
243 34
			return _json_decode($value);
244
		}
245 48
		if (strpos($model, 'int') === 0) {
246 40
			return (int)$value;
247
		}
248 46
		if (strpos($model, 'float') === 0) {
249 2
			return (float)$value;
250
		}
251 46
		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 6
	private function read_joined_table ($id, $table, $model, $force_clang = null) {
262 6
		$clang                    = $force_clang ?: $this->db()->s(Language::instance()->clang, false);
263 6
		$id_field                 = array_keys($model['data_model'])[0];
264 6
		$language_field_condition = isset($model['language_field'])
265 4
			? "AND `$model[language_field]` = '$clang'"
266 6
			: '';
267 6
		$fields                   = '`'.implode('`,`', array_keys($model['data_model'])).'`';
268 6
		$rows                     = $this->db_prime()->qfa(
269 6
			"SELECT $fields
270 6
			FROM `{$this->table}_$table`
271
			WHERE
272 6
				`$id_field`	= ?
273 6
				$language_field_condition",
274
			$id
275 6
		) ?: [];
276 6
		$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 6
		if (!$rows && $language_field !== null) {
281 4
			$new_clang = $this->db_prime()->qfs(
282 4
				"SELECT `$language_field`
283 4
				FROM `{$this->table}_$table`
284 4
				WHERE `$id_field`	= ?
285
				LIMIT 1",
286
				$id
287
			);
288 4
			if ($new_clang && $new_clang != $clang) {
289 4
				return $this->read_joined_table($id, $table, $model, $new_clang);
290
			}
291
			return [];
292
		}
293 6
		foreach ($rows as &$row) {
294
			/**
295
			 * Drop language and id field since they are used internally, not specified by user
296
			 */
297
			unset(
298 6
				$row[$language_field],
299 6
				$row[$id_field]
300
			);
301 6
			foreach ($row as $field => &$value) {
302 6
				$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 6
			if (count($row) == 1) {
308 6
				$row = array_pop($row);
309
			}
310
		}
311 6
		return $rows;
312
	}
313
	/**
314
	 * Update item
315
	 *
316
	 * @param array $arguments
317
	 *
318
	 * @return bool
319
	 */
320 18
	protected function update (...$arguments) {
321 18
		if (count($arguments) == 1) {
322 10
			$arguments = $arguments[0];
323
		}
324
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
325 18
		$this->db_prime()->transaction(
326
			function () use (&$result, $arguments) {
327 18
				$result = $this->update_internal($this->table, $this->data_model, $arguments);
328 18
			}
329
		);
330 18
		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 18
	private function update_internal ($table, $data_model, $arguments, $files_update = true) {
343 18
		$arguments          = $this->fix_arguments_order($data_model, $arguments);
344 18
		$prepared_arguments = $arguments;
345 18
		$id                 = array_shift($prepared_arguments);
346 18
		$data               = $this->read_internal($table, $data_model, $id);
347 18
		if (!$data) {
348 2
			return false;
349
		}
350 18
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(array_slice($data_model, 1), $prepared_arguments, $id);
351 18
		$columns              = implode(
352 18
			',',
353
			array_map(
354
				function ($column) {
355 18
					return "`$column` = ?";
356 18
				},
357
				array_keys($prepared_arguments)
358
			)
359
		);
360 18
		$prepared_arguments[] = $id;
361 18
		$first_column         = array_keys($data_model)[0];
362 18
		if (!$this->db_prime()->q(
363 18
			"UPDATE `$table`
364 18
			SET $columns
365 18
			WHERE `$first_column` = ?",
366
			$prepared_arguments
367
		)
368
		) {
369
			return false;
370
		}
371 18
		if ($files_update) {
372 18
			$this->update_joined_tables($id, $joined_tables);
373 18
			$this->find_update_files_tags($id, $data, $arguments);
374
		}
375 18
		return true;
376
	}
377
	/**
378
	 * Delete item
379
	 *
380
	 * @param int|int[]|string|string[] $id
381
	 *
382
	 * @return bool
383
	 */
384 20
	protected function delete ($id) {
385
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
386 20
		$this->db_prime()->transaction(
387 20
			function () use (&$result, $id) {
388 20
				$result = $this->delete_internal($this->table, $this->data_model, $id);
389 20
			}
390
		);
391 20
		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 20
	private function delete_internal ($table, $data_model, $id) {
403 20
		$id           = (array)$id;
404 20
		$result       = true;
405 20
		$multilingual = $this->is_multilingual();
406 20
		$first_column = array_keys($data_model)[0];
407 20
		foreach ($id as $i) {
408
			$result =
409 20
				$result &&
410 20
				$this->read_internal($table, $data_model, $i) &&
411 20
				$this->db_prime()->q(
412 20
					"DELETE FROM `$table`
413 20
					WHERE `$first_column` = ?",
414
					$i
415
				);
416
			/**
417
			 * If there are multilingual fields - handle multilingual deleting of fields automatically
418
			 */
419 20
			if ($multilingual) {
420 2
				foreach (array_keys($this->data_model) as $field) {
421 2
					if (is_string($this->data_model[$field]) && strpos($this->data_model[$field], 'ml:') === 0) {
422 2
						Text::instance()->del($this->cdb(), "$this->data_model_ml_group/$field", $i);
423
					}
424
				}
425
			}
426 20
			$this->update_joined_tables($i, []);
427 20
			$this->delete_files_tags($i);
428
		}
429 20
		return $result;
430
	}
431
}
432