Completed
Push — master ( 215c97...c1aac3 )
by Nazar
04:30
created

CRUD::update_internal()   B

Complexity

Conditions 4
Paths 4

Size

Total Lines 35
Code Lines 26

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 22
CRAP Score 4.0012

Importance

Changes 0
Metric Value
cc 4
eloc 26
nc 4
nop 4
dl 0
loc 35
ccs 22
cts 23
cp 0.9565
crap 4.0012
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-2016, 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 30
	protected function create (...$arguments) {
30 30
		if (count($arguments) == 1 && !is_array(array_values($this->data_model)[1])) {
31 24
			$arguments = $arguments[0];
32
		}
33
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
34 30
		$this->db_prime()->transaction(
35
			function () use (&$id, $arguments) {
36 30
				$id = $this->create_internal($this->table, $this->data_model, $arguments);
37 30
			}
38
		);
39 30
		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 30
	private function create_internal ($table, $data_model, $arguments) {
51 30
		$arguments = $this->fix_arguments_order($data_model, $arguments);
52 30
		$insert_id = count($data_model) == count($arguments) ? $arguments[0] : false;
53 30
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(
54 30
			$insert_id !== false ? $data_model : array_slice($data_model, 1),
55
			$arguments,
56
			$insert_id,
57
			$update_needed
58
		);
59 30
		$columns = "`".implode("`,`", array_keys($prepared_arguments))."`";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal ` does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal `,` does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
60 30
		$values  = implode(',', array_fill(0, count($prepared_arguments), "'%s'"));
61 30
		$return  = $this->db_prime()->q(
62 30
			"INSERT IGNORE INTO `$table`
63
				(
64 30
					$columns
65
				) VALUES (
66 30
					$values
67 30
				)",
68
			$prepared_arguments
69
		);
70 30
		$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 30
		if (!$return || $id === 0) {
75
			return false;
76
		}
77 30
		$this->update_joined_tables($id, $joined_tables);
78 30
		$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 30
		if ($update_needed) {
84 2
			$this->update_internal(
85
				$table,
86
				array_filter(
87
					$data_model,
88
					function ($item) {
89 2
						return !is_array($item) || !isset($item['data_model']);
90 2
					}
91
				),
92 2
				array_merge([array_keys($data_model)[0] => $id], $prepared_arguments),
93 2
				false
94
			);
95
		}
96 30
		return $id;
97
	}
98
	/**
99
	 * @param int|string $id
100
	 * @param array      $joined_tables
101
	 */
102 32
	private function update_joined_tables ($id, $joined_tables) {
103 32
		$clang = $this->db_prime()->s(Language::instance()->clang, false);
104
		/**
105
		 * At first we remove all old data
106
		 */
107 32
		foreach ($this->data_model as $table => $model) {
108 32
			if (!is_array($model) || !isset($model['data_model'])) {
109 32
				continue;
110
			}
111 4
			$id_field                 = array_keys($model['data_model'])[0];
112 4
			$language_field_condition = isset($model['language_field'])
113 2
				? "AND `$model[language_field]` = '$clang'"
114 4
				: '';
115 4
			$this->db_prime()->q(
116 4
				"DELETE FROM `{$this->table}_$table`
117
				WHERE
118 4
					`$id_field`	= '%s'
119 4
					$language_field_condition",
120
				$id
121
			);
122
		}
123 32
		$id = $this->db_prime()->s($id, false);
124
		/**
125
		 * Now walk through all tables and insert new valued
126
		 */
127 32
		foreach ($joined_tables as $table => $model) {
128 4
			if (!@$model['data']) {
129 2
				continue;
130
			}
131 4
			$fields = "`$model[id_field]`, ";
132
			/** @noinspection DisconnectedForeachInstructionInspection */
133 4
			$values = "'$id'";
134 4
			if (isset($model['language_field'])) {
135 2
				$fields .= "`$model[language_field]`, ";
136 2
				$values .= ",'$clang'";
137
			}
138 4
			$fields .= '`'.implode('`,`', array_keys($model['fields'])).'`';
139 4
			$values .= str_repeat(",'%s'", count($model['fields']));
140 4
			$this->db_prime()->insert(
141 4
				"INSERT INTO `{$this->table}_$table`
142
					(
143 4
						$fields
144
					) VALUES (
145 4
						$values
146 4
					)",
147 4
				$model['data']
148
			);
149
		}
150 32
	}
151
	/**
152
	 * Read item
153
	 *
154
	 * @param int|int[]|string|string[] $id
155
	 *
156
	 * @return array|false
157
	 */
158 20
	protected function read ($id) {
159
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
160 20
		$this->db()->transaction(
161
			function () use (&$result, $id) {
162 20
				$result = $this->read_internal($this->table, $this->data_model, $id);
163 20
			}
164
		);
165 20
		return $result;
166
	}
167
	/**
168
	 * Read item
169
	 *
170
	 * @param string                    $table
171
	 * @param callable[]|string[]       $data_model
172
	 * @param int|int[]|string|string[] $id
173
	 *
174
	 * @return array|false
175
	 */
176 28
	private function read_internal ($table, $data_model, $id) {
177 28
		if (is_array($id)) {
178 8
			foreach ($id as &$i) {
179 8
				$i = $this->read_internal($table, $data_model, $i);
180
			}
181 8
			return $id;
182
		}
183 28
		$columns      = array_filter(
184
			$data_model,
185
			function ($column) {
186 28
				return !is_array($column) || !isset($column['data_model']);
187 28
			}
188
		);
189 28
		$columns      = "`".implode("`,`", array_keys($columns))."`";
0 ignored issues
show
Coding Style Comprehensibility introduced by
The string literal ` does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
Coding Style Comprehensibility introduced by
The string literal `,` does not require double quotes, as per coding-style, please use single quotes.

PHP provides two ways to mark string literals. Either with single quotes 'literal' or with double quotes "literal". The difference between these is that string literals in double quotes may contain variables with are evaluated at run-time as well as escape sequences.

String literals in single quotes on the other hand are evaluated very literally and the only two characters that needs escaping in the literal are the single quote itself (\') and the backslash (\\). Every other character is displayed as is.

Double quoted string literals may contain other variables or more complex escape sequences.

<?php

$singleQuoted = 'Value';
$doubleQuoted = "\tSingle is $singleQuoted";

print $doubleQuoted;

will print an indented: Single is Value

If your string literal does not contain variables or escape sequences, it should be defined using single quotes to make that fact clear.

For more information on PHP string literals and available escape sequences see the PHP core documentation.

Loading history...
190 28
		$first_column = array_keys($data_model)[0];
191 28
		$data         = $this->db()->qf(
192 28
			"SELECT $columns
193 28
			FROM `$table`
194 28
			WHERE `$first_column` = '%s'
195 28
			LIMIT 1",
196
			$id
197
		);
198 28
		if (!$data) {
199 10
			return false;
200
		}
201 28
		foreach ($this->data_model as $field => $model) {
202 28
			if (is_array($model) && isset($model['data_model'])) {
203 4
				$data[$field] = $this->read_joined_table($id, $field, $model);
204
			} else {
205 28
				if (is_string($model)) {
206
					/**
207
					 * Handle multilingual fields automatically
208
					 */
209 28
					if (strpos($model, 'ml:') === 0) {
210 2
						$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
211
					}
212
				}
213 28
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
214
			}
215
		}
216 28
		return $data;
217
	}
218
	/**
219
	 * @param false|string|string[] $value
220
	 * @param string                $model
221
	 *
222
	 * @return array|false|float|int|string
223
	 */
224 28
	private function read_field_post_processing ($value, $model) {
225 28
		if (is_callable($model)) {
226 4
			return $model($value);
227
		}
228
		/**
229
		 * Decode JSON fields
230
		 */
231 28
		if (in_array($model, ['json', 'ml:json'])) {
232 18
			return _json_decode($value);
233
		}
234 28
		if (strpos($model, 'int') === 0) {
235 24
			return _int($value);
236
		}
237 28
		if (strpos($model, 'float') === 0) {
238
			return _float($value);
239
		}
240 28
		return $value;
241
	}
242
	/**
243
	 * @param int|string  $id
244
	 * @param string      $table
245
	 * @param array       $model
246
	 * @param null|string $force_clang
247
	 *
248
	 * @return array
249
	 */
250 4
	private function read_joined_table ($id, $table, $model, $force_clang = null) {
251 4
		$clang                    = $force_clang ?: $this->db()->s(Language::instance()->clang, false);
252 4
		$id_field                 = array_keys($model['data_model'])[0];
253 4
		$language_field_condition = isset($model['language_field'])
254 2
			? "AND `$model[language_field]` = '$clang'"
255 4
			: '';
256 4
		$fields                   = '`'.implode('`,`', array_keys($model['data_model'])).'`';
257 4
		$rows                     = $this->db_prime()->qfa(
258 4
			"SELECT $fields
259 4
			FROM `{$this->table}_$table`
260
			WHERE
261 4
				`$id_field`	= '%s'
262 4
				$language_field_condition",
263
			$id
264 4
		) ?: [];
265 4
		$language_field           = isset($model['language_field']) ? $model['language_field'] : null;
266
		/**
267
		 * If no rows found for current language - find another language that should contain some rows
268
		 */
269 4
		if (!$rows && $language_field !== null) {
270 2
			$new_clang = $this->db_prime()->qfs(
271 2
				"SELECT `$language_field`
272 2
				FROM `{$this->table}_$table`
273 2
				WHERE `$id_field`	= '%s'
274 2
				LIMIT 1",
275
				$id
276
			);
277 2
			if ($new_clang && $new_clang != $clang) {
278 2
				return $this->read_joined_table($id, $table, $model, $new_clang);
279
			}
280
			return [];
281
		}
282 4
		foreach ($rows as &$row) {
283
			/**
284
			 * Drop language and id field since they are used internally, not specified by user
285
			 */
286
			unset(
287 4
				$row[$language_field],
288 4
				$row[$id_field]
289
			);
290 4
			foreach ($row as $field => &$value) {
291 4
				$value = $this->read_field_post_processing($value, $model['data_model'][$field]);
292
			}
293
			/**
294
			 * If row is array that contains only one item - lets make resulting array flat
295
			 */
296 4
			if (count($row) == 1) {
297 4
				$row = array_pop($row);
298
			}
299
		}
300 4
		return $rows;
301
	}
302
	/**
303
	 * Update item
304
	 *
305
	 * @param array $arguments
306
	 *
307
	 * @return bool
308
	 */
309 14
	protected function update (...$arguments) {
310 14
		if (count($arguments) == 1) {
311 8
			$arguments = $arguments[0];
312
		}
313
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
314 14
		$this->db_prime()->transaction(
315
			function () use (&$result, $arguments) {
316 14
				$result = $this->update_internal($this->table, $this->data_model, $arguments);
317 14
			}
318
		);
319 14
		return $result;
320
	}
321
	/**
322
	 * Update item
323
	 *
324
	 * @param string              $table
325
	 * @param callable[]|string[] $data_model
326
	 * @param array               $arguments
327
	 * @param bool                $files_update
328
	 *
329
	 * @return bool
330
	 */
331 14
	private function update_internal ($table, $data_model, $arguments, $files_update = true) {
332 14
		$arguments          = $this->fix_arguments_order($data_model, $arguments);
333 14
		$prepared_arguments = $arguments;
334 14
		$id                 = array_shift($prepared_arguments);
335 14
		$data               = $this->read_internal($table, $data_model, $id);
336 14
		if (!$data) {
337 2
			return false;
338
		}
339 14
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(array_slice($data_model, 1), $prepared_arguments, $id);
340 14
		$columns              = implode(
341 14
			',',
342
			array_map(
343
				function ($column) {
344 14
					return "`$column` = '%s'";
345 14
				},
346
				array_keys($prepared_arguments)
347
			)
348
		);
349 14
		$prepared_arguments[] = $id;
350 14
		$first_column         = array_keys($data_model)[0];
351 14
		if (!$this->db_prime()->q(
352 14
			"UPDATE `$table`
353 14
			SET $columns
354 14
			WHERE `$first_column` = '%s'",
355
			$prepared_arguments
356
		)
357
		) {
358
			return false;
359
		}
360 14
		if ($files_update) {
361 14
			$this->update_joined_tables($id, $joined_tables);
362 14
			$this->find_update_files_tags($id, $data, $arguments);
363
		}
364 14
		return true;
365
	}
366
	/**
367
	 * Delete item
368
	 *
369
	 * @param int|int[]|string|string[] $id
370
	 *
371
	 * @return bool
372
	 */
373 14
	protected function delete ($id) {
374
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
375 14
		$this->db_prime()->transaction(
376 14
			function () use (&$result, $id) {
377 14
				$result = $this->delete_internal($this->table, $this->data_model, $id);
378 14
			}
379
		);
380 14
		return $result;
381
	}
382
	/**
383
	 * Delete item
384
	 *
385
	 * @param string                    $table
386
	 * @param callable[]|string[]       $data_model
387
	 * @param int|int[]|string|string[] $id
388
	 *
389
	 * @return bool
390
	 */
391 14
	private function delete_internal ($table, $data_model, $id) {
392 14
		$id           = (array)$id;
393 14
		$result       = true;
394 14
		$multilingual = $this->is_multilingual();
395 14
		$first_column = array_keys($data_model)[0];
396 14
		foreach ($id as $i) {
397
			$result =
398 14
				$result &&
399 14
				$this->read_internal($table, $data_model, $i) &&
400 14
				$this->db_prime()->q(
401 14
					"DELETE FROM `$table`
402 14
					WHERE `$first_column` = '%s'",
403
					$i
404
				);
405
			/**
406
			 * If there are multilingual fields - handle multilingual deleting of fields automatically
407
			 */
408 14
			if ($multilingual) {
409 2
				foreach (array_keys($this->data_model) as $field) {
410 2
					if (is_string($this->data_model[$field]) && strpos($this->data_model[$field], 'ml:') === 0) {
411 2
						Text::instance()->del($this->cdb(), "$this->data_model_ml_group/$field", $i);
412
					}
413
				}
414
			}
415 14
			$this->update_joined_tables($i, []);
416 14
			$this->delete_files_tags($i);
417
		}
418 14
		return $result;
419
	}
420
}
421