Completed
Push — master ( aeb93c...90f2e6 )
by Nazar
04:15
created

CRUD::update()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %
Metric Value
dl 0
loc 3
rs 10
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * @package   CleverStyle CMS
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
	protected function create ($arguments) {
30
		return $this->create_internal($this->table, $this->data_model, $arguments);
31
	}
32
	/**
33
	 * Create item
34
	 *
35
	 * @param string              $table
36
	 * @param callable[]|string[] $data_model
37
	 * @param array               $arguments First element `id` can be omitted if it is autoincrement field
38
	 *
39
	 * @return false|int|string Id of created item on success (or specified primary key), `false` otherwise
40
	 */
41
	private function create_internal ($table, $data_model, $arguments) {
42
		$arguments = $this->fix_arguments_order($data_model, $arguments);
43
		$insert_id = count($data_model) == count($arguments) ? $arguments[0] : false;
44
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(
45
			$insert_id !== false ? $data_model : array_slice($data_model, 1),
46
			$arguments,
47
			$insert_id,
48
			$update_needed
49
		);
50
		$columns = "`".implode("`,`", array_keys($prepared_arguments))."`";
51
		$values  = implode(',', array_fill(0, count($prepared_arguments), "'%s'"));
52
		$return  = $this->db_prime()->q(
53
			"INSERT IGNORE INTO `$table`
54
				(
55
					$columns
56
				) VALUES (
57
					$values
58
				)",
59
			$prepared_arguments
60
		);
61
		$id      = $insert_id !== false ? $insert_id : $this->db_prime()->id();
62
		/**
63
		 * Id might be 0 if insertion failed or if we insert duplicate entry (which is fine since we use 'INSERT IGNORE'
64
		 */
65
		if (!$return || $id === 0) {
66
			return false;
67
		}
68
		$this->update_joined_tables($id, $joined_tables);
69
		$this->find_update_files_tags($id, [], $arguments);
70
		/**
71
		 * If on creation request without specified primary key and multilingual fields present - update needed
72
		 * after creation (there is no id before creation)
73
		 */
74
		if ($update_needed) {
75
			$this->update_internal($table, $data_model, array_merge([array_keys($data_model)[0] => $id], $prepared_arguments), false);
76
		}
77
		return $id;
78
	}
79
	/**
80
	 * @param int|string $id
81
	 * @param array      $joined_tables
82
	 */
83
	private function update_joined_tables ($id, $joined_tables) {
84
		$clang = $this->db_prime()->s(Language::instance()->clang, false);
85
		/**
86
		 * At first we remove all old data
87
		 */
88
		foreach ($this->data_model as $table => $model) {
89
			if (!is_array($model) || !isset($model['data_model'])) {
90
				continue;
91
			}
92
			$id_field                 = array_keys($model['data_model'])[0];
93
			$language_field_condition = isset($model['language_field'])
94
				? "AND `$model[language_field]` = '$clang'"
95
				: '';
96
			$this->db_prime()->q(
97
				"DELETE FROM `{$this->table}_$table`
98
				WHERE
99
					`$id_field`	= '%s'
100
					$language_field_condition",
101
				$id
102
			);
103
		}
104
		$id = $this->db_prime()->s($id, false);
105
		/**
106
		 * Now walk through all tables and insert new valued
107
		 */
108
		foreach ($joined_tables as $table => $model) {
109
			if (!@$model['data']) {
110
				continue;
111
			}
112
			$fields = "`$model[id_field]`, ";
113
			/** @noinspection DisconnectedForeachInstructionInspection */
114
			$values = "'$id'";
115
			if (isset($model['language_field'])) {
116
				$fields .= "`$model[language_field]`, ";
117
				$values .= ",'$clang'";
118
			}
119
			$fields .= '`'.implode('`,`', array_keys($model['fields'])).'`';
120
			$values .= str_repeat(",'%s'", count($model['fields']));
121
			$this->db_prime()->insert(
122
				"INSERT INTO `{$this->table}_$table`
123
					(
124
						$fields
125
					) VALUES (
126
						$values
127
					)",
128
				$model['data']
129
			);
130
		}
131
	}
132
	/**
133
	 * Read item
134
	 *
135
	 * @param int|int[]|string|string[] $id
136
	 *
137
	 * @return array|false
1 ignored issue
show
Documentation introduced by
Should the return type not be array|false|integer|string? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
138
	 */
139
	protected function read ($id) {
140
		return $this->read_internal($this->table, $this->data_model, $id);
141
	}
142
	/**
143
	 * Read item
144
	 *
145
	 * @param string                    $table
146
	 * @param callable[]|string[]       $data_model
147
	 * @param int|int[]|string|string[] $id
148
	 *
149
	 * @return array|false
1 ignored issue
show
Documentation introduced by
Should the return type not be array|false|integer|string? Also, consider making the array more specific, something like array<String>, or String[].

This check compares the return type specified in the @return annotation of a function or method doc comment with the types returned by the function and raises an issue if they mismatch.

If the return type contains the type array, this check recommends the use of a more specific type like String[] or array<String>.

Loading history...
150
	 */
151
	private function read_internal ($table, $data_model, $id) {
152
		if (is_array($id)) {
153
			foreach ($id as &$i) {
154
				$i = $this->read_internal($table, $data_model, $i);
155
			}
156
			return $id;
157
		}
158
		$columns      = array_filter(
159
			$data_model,
160
			function ($column) {
161
				return !is_array($column) || !isset($column['data_model']);
162
			}
163
		);
164
		$columns      = "`".implode("`,`", array_keys($columns))."`";
165
		$first_column = array_keys($data_model)[0];
166
		$data         = $this->db()->qf(
167
			"SELECT $columns
168
			FROM `$table`
169
			WHERE `$first_column` = '%s'
170
			LIMIT 1",
171
			$id
172
		);
173
		if (!$data) {
174
			return false;
175
		}
176
		foreach ($this->data_model as $field => $model) {
177
			if (is_string($model)) {
178
				/**
179
				 * Handle multilingual fields automatically
180
				 */
181
				if (strpos($model, 'ml:') === 0) {
182
					$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
183
				}
184
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
185
			} elseif (is_array($model) && isset($model['data_model'])) {
186
				$data[$field] = $this->read_joined_table($id, $field, $model);
187
			}
188
		}
189
		return $data;
190
	}
191
	/**
192
	 * @param mixed  $value
193
	 * @param string $model
194
	 *
195
	 * @return mixed
196
	 */
197
	private function read_field_post_processing ($value, $model) {
198
		/**
199
		 * Decode JSON fields
200
		 */
201
		if (in_array($model, ['json', 'ml:json'])) {
202
			return _json_decode($value);
203
		}
204
		if (strpos($model, 'int') === 0) {
205
			return (int)$value;
206
		}
207
		if (strpos($model, 'float') === 0) {
208
			return (float)$value;
209
		}
210
		return $value;
211
	}
212
	/**
213
	 * @param int|string  $id
214
	 * @param string      $table
215
	 * @param array       $model
216
	 * @param null|string $force_clang
217
	 *
218
	 * @return array
219
	 */
220
	private function read_joined_table ($id, $table, $model, $force_clang = null) {
221
		$clang                    = $force_clang ?: $this->db()->s(Language::instance()->clang, false);
222
		$id_field                 = array_keys($model['data_model'])[0];
223
		$language_field_condition = isset($model['language_field'])
224
			? "AND `$model[language_field]` = '$clang'"
225
			: '';
226
		$fields                   = '`'.implode('`,`', array_keys($model['data_model'])).'`';
227
		$rows                     = $this->db_prime()->qfa(
228
			"SELECT $fields
229
			FROM `{$this->table}_$table`
230
			WHERE
231
					`$id_field`	= '%s'
232
					$language_field_condition",
233
			$id
234
		) ?: [];
235
		$language_field           = isset($model['language_field']) ? $model['language_field'] : null;
236
		/**
237
		 * If no rows found for current language - find another language that should contain some rows
238
		 */
239
		if (!$rows) {
240
			$new_clang = $this->db_prime()->qfs(
241
				"SELECT `$language_field`
242
				FROM `{$this->table}_$table`
243
				WHERE `$id_field`	= '%s'
244
				LIMIT 1",
245
				$id
246
			);
247
			if ($new_clang && $new_clang != $clang) {
248
				return $this->read_joined_table($id, $table, $model, $force_clang);
249
			}
250
			return [];
251
		}
252
		foreach ($rows as &$row) {
253
			/**
254
			 * Drop language and id field since they are used internally, not specified by user
255
			 */
256
			unset(
257
				$row[$language_field],
258
				$row[$id_field]
259
			);
260
			foreach ($row as $field => &$value) {
261
				$value = $this->read_field_post_processing($value, $model);
0 ignored issues
show
Documentation introduced by
$model is of type array<string,?,{"language_field":"?"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
262
			}
263
			if (isset($model['indexed']) && $model['indexed']) {
264
				$row = array_values($row);
265
			} elseif (count($row) == 1) {
266
				/**
267
				 * If row is array that contains only one item - lets make resulting array flat
268
				 */
269
				$row = array_pop($row);
270
			}
271
		}
272
		return $rows;
273
	}
274
	/**
275
	 * Update item
276
	 *
277
	 * @param array $arguments
278
	 *
279
	 * @return bool
280
	 */
281
	protected function update ($arguments) {
282
		return $this->update_internal($this->table, $this->data_model, $arguments);
283
	}
284
	/**
285
	 * Update item
286
	 *
287
	 * @param string              $table
288
	 * @param callable[]|string[] $data_model
289
	 * @param array               $arguments
290
	 * @param bool                $files_update
291
	 *
292
	 * @return bool
293
	 */
294
	private function update_internal ($table, $data_model, $arguments, $files_update = true) {
295
		$arguments          = $this->fix_arguments_order($data_model, $arguments);
296
		$prepared_arguments = $arguments;
297
		$id                 = array_shift($prepared_arguments);
298
		if ($files_update) {
299
			$data_before = $this->read_internal($table, $data_model, $id);
300
		}
301
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(array_slice($data_model, 1), $prepared_arguments, $id);
302
		$columns              = implode(
303
			',',
304
			array_map(
305
				function ($column) {
306
					return "`$column` = '%s'";
307
				},
308
				array_keys($prepared_arguments)
309
			)
310
		);
311
		$prepared_arguments[] = $id;
312
		$first_column         = array_keys($data_model)[0];
313
		if (!$this->db_prime()->q(
314
			"UPDATE `$table`
315
			SET $columns
316
			WHERE `$first_column` = '%s'
317
			LIMIT 1",
318
			$prepared_arguments
319
		)
320
		) {
321
			return false;
322
		}
323
		if ($files_update) {
324
			$this->update_joined_tables($id, $joined_tables);
325
			/** @noinspection PhpUndefinedVariableInspection */
326
			$this->find_update_files_tags($id, $data_before, $arguments);
327
		}
328
		return true;
329
	}
330
	/**
331
	 * Delete item
332
	 *
333
	 * @param int|int[]|string|string[] $id
334
	 *
335
	 * @return bool
336
	 */
337
	protected function delete ($id) {
338
		return $this->delete_internal($this->table, $this->data_model, $id);
339
	}
340
	/**
341
	 * Delete item
342
	 *
343
	 * @param string                    $table
344
	 * @param callable[]|string[]       $data_model
345
	 * @param int|int[]|string|string[] $id
346
	 *
347
	 * @return bool
348
	 */
349
	private function delete_internal ($table, $data_model, $id) {
350
		$id           = (array)$id;
351
		$result       = true;
352
		$multilingual = $this->is_multilingual();
353
		$first_column = array_keys($data_model)[0];
354
		foreach ($id as $i) {
355
			$result =
356
				$result &&
357
				$this->db_prime()->q(
358
					"DELETE FROM `$table`
359
					WHERE `$first_column` = '%s'
360
					LIMIT 1",
361
					$i
362
				);
363
			/**
364
			 * If there are multilingual fields - handle multilingual deleting of fields automatically
365
			 */
366
			if ($multilingual) {
367
				/** @noinspection ForeachOnArrayComponentsInspection */
368
				foreach (array_keys($this->data_model) as $field) {
369
					if (strpos($this->data_model[$field], 'ml:') === 0) {
370
						Text::instance()->del($this->cdb(), "$this->data_model_ml_group/$field", $i);
371
					}
372
				}
373
			}
374
			$this->update_joined_tables($i, []);
375
			$this->delete_files_tags($i);
376
		}
377
		return $result;
378
	}
379
}
380