Completed
Push — master ( 068c90...e4afe1 )
by Nazar
04:06
created

CRUD::format_without_data_model()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 1
dl 0
loc 3
ccs 2
cts 2
cp 1
crap 1
rs 10
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 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 44
				)",
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
			/** @noinspection DisconnectedForeachInstructionInspection */
144 8
			$values = "'$id'";
145 8
			if (isset($model['language_field'])) {
146 4
				$fields .= "`$model[language_field]`, ";
147 4
				$values .= ",'$clang'";
148
			}
149 8
			$fields .= '`'.implode('`,`', array_keys($model['fields'])).'`';
150 8
			$values .= str_repeat(',?', count($model['fields']));
151 8
			$this->db_prime()->insert(
152 8
				"INSERT INTO `{$this->table}_$table`
153
					(
154 8
						$fields
155
					) VALUES (
156 8
						$values
157 8
					)",
158 8
				$model['data']
159
			);
160
		}
161 48
	}
162
	/**
163
	 * Read item
164
	 *
165
	 * @param int|int[]|string|string[] $id
166
	 *
167
	 * @return array|false
168
	 */
169 34
	protected function read ($id) {
170
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
171 34
		$this->db()->transaction(
172
			function () use (&$result, $id) {
173 34
				$result = $this->read_internal($this->table, $this->data_model, $id);
174 34
			}
175
		);
176 34
		return $result;
177
	}
178
	/**
179
	 * Read item
180
	 *
181
	 * @param string                    $table
182
	 * @param callable[]|string[]       $data_model
183
	 * @param int|int[]|string|string[] $id
184
	 *
185
	 * @return array|false
186
	 */
187 46
	private function read_internal ($table, $data_model, $id) {
188 46
		if (is_array($id)) {
189 8
			foreach ($id as &$i) {
190 8
				$i = $this->read_internal($table, $data_model, $i);
191
			}
192 8
			return $id;
193
		}
194 46
		$columns      = array_filter($data_model, [$this, 'format_without_data_model']);
195 46
		$columns      = '`'.implode('`,`', array_keys($columns)).'`';
196 46
		$first_column = array_keys($data_model)[0];
197 46
		$data         = $this->db()->qf(
198 46
			"SELECT $columns
199 46
			FROM `$table`
200 46
			WHERE `$first_column` = ?
201 46
			LIMIT 1",
202
			$id
203
		);
204 46
		if (!$data) {
205 10
			return false;
206
		}
207 46
		foreach ($this->data_model as $field => $model) {
208 46
			if ($this->format_with_data_model($model)) {
209 6
				$data[$field] = $this->read_joined_table($id, $field, $model);
210
			} else {
211 46
				if (is_string($model)) {
212
					/**
213
					 * Handle multilingual fields automatically
214
					 */
215 46
					if (strpos($model, 'ml:') === 0) {
216 4
						$data[$field] = Text::instance()->process($this->cdb(), $data[$field], true);
217
					}
218
				}
219 46
				$data[$field] = $this->read_field_post_processing($data[$field], $model);
220
			}
221
		}
222 46
		return $data;
223
	}
224
	/**
225
	 * @param false|string|string[] $value
226
	 * @param callable|string       $model
227
	 *
228
	 * @return array|false|float|int|string
229
	 */
230 48
	private function read_field_post_processing ($value, $model) {
231 48
		if (is_array($value)) {
232 12
			foreach ($value as &$v) {
233 12
				$v = $this->read_field_post_processing($v, $model);
234
			}
235 12
			return $value;
236
		}
237 48
		if (is_callable($model)) {
238 6
			return $model($value);
239
		}
240
		/**
241
		 * Decode JSON fields
242
		 */
243 48
		if (in_array($model, ['json', 'ml:json'])) {
244 34
			return _json_decode($value);
245
		}
246 48
		if (strpos($model, 'int') === 0) {
247 40
			return (int)$value;
248
		}
249 46
		if (strpos($model, 'float') === 0) {
250 2
			return (float)$value;
251
		}
252 46
		return $value;
253
	}
254
	/**
255
	 * @param int|string  $id
256
	 * @param string      $table
257
	 * @param array       $model
258
	 * @param null|string $force_clang
259
	 *
260
	 * @return array
261
	 */
262 6
	private function read_joined_table ($id, $table, $model, $force_clang = null) {
263 6
		$clang                    = $force_clang ?: $this->db()->s(Language::instance()->clang, false);
264 6
		$id_field                 = array_keys($model['data_model'])[0];
265 6
		$language_field_condition = isset($model['language_field'])
266 4
			? "AND `$model[language_field]` = '$clang'"
267 6
			: '';
268 6
		$fields                   = '`'.implode('`,`', array_keys($model['data_model'])).'`';
269 6
		$rows                     = $this->db_prime()->qfa(
270 6
			"SELECT $fields
271 6
			FROM `{$this->table}_$table`
272
			WHERE
273 6
				`$id_field`	= ?
274 6
				$language_field_condition",
275
			$id
276 6
		) ?: [];
277 6
		$language_field           = isset($model['language_field']) ? $model['language_field'] : null;
278
		/**
279
		 * If no rows found for current language - find another language that should contain some rows
280
		 */
281 6
		if (!$rows && $language_field !== null) {
282 4
			$new_clang = $this->db_prime()->qfs(
283 4
				"SELECT `$language_field`
284 4
				FROM `{$this->table}_$table`
285 4
				WHERE `$id_field`	= ?
286 4
				LIMIT 1",
287
				$id
288
			);
289 4
			if ($new_clang && $new_clang != $clang) {
290 4
				return $this->read_joined_table($id, $table, $model, $new_clang);
291
			}
292
			return [];
293
		}
294 6
		foreach ($rows as &$row) {
295
			/**
296
			 * Drop language and id field since they are used internally, not specified by user
297
			 */
298
			unset(
299 6
				$row[$language_field],
300 6
				$row[$id_field]
301
			);
302 6
			foreach ($row as $field => &$value) {
303 6
				$value = $this->read_field_post_processing($value, $model['data_model'][$field]);
304
			}
305
			/**
306
			 * If row is array that contains only one item - lets make resulting array flat
307
			 */
308 6
			if (count($row) == 1) {
309 6
				$row = array_pop($row);
310
			}
311
		}
312 6
		return $rows;
313
	}
314
	/**
315
	 * Update item
316
	 *
317
	 * @param array $arguments
318
	 *
319
	 * @return bool
320
	 */
321 18
	protected function update (...$arguments) {
322 18
		if (count($arguments) == 1) {
323 10
			$arguments = $arguments[0];
324
		}
325
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
326 18
		$this->db_prime()->transaction(
327
			function () use (&$result, $arguments) {
328 18
				$result = $this->update_internal($this->table, $this->data_model, $arguments);
329 18
			}
330
		);
331 18
		return $result;
332
	}
333
	/**
334
	 * Update item
335
	 *
336
	 * @param string              $table
337
	 * @param callable[]|string[] $data_model
338
	 * @param array               $arguments
339
	 * @param bool                $files_update
340
	 *
341
	 * @return bool
342
	 */
343 18
	private function update_internal ($table, $data_model, $arguments, $files_update = true) {
344 18
		$arguments          = $this->fix_arguments_order($data_model, $arguments);
345 18
		$prepared_arguments = $arguments;
346 18
		$id                 = array_shift($prepared_arguments);
347 18
		$data               = $this->read_internal($table, $data_model, $id);
348 18
		if (!$data) {
349 2
			return false;
350
		}
351 18
		list($prepared_arguments, $joined_tables) = $this->crud_arguments_preparation(array_slice($data_model, 1), $prepared_arguments, $id);
352 18
		$columns              = implode(
353 18
			',',
354
			array_map(
355
				function ($column) {
356 18
					return "`$column` = ?";
357 18
				},
358
				array_keys($prepared_arguments)
359
			)
360
		);
361 18
		$prepared_arguments[] = $id;
362 18
		$first_column         = array_keys($data_model)[0];
363 18
		if (!$this->db_prime()->q(
364 18
			"UPDATE `$table`
365 18
			SET $columns
366 18
			WHERE `$first_column` = ?",
367
			$prepared_arguments
368
		)
369
		) {
370
			return false;
371
		}
372 18
		if ($files_update) {
373 18
			$this->update_joined_tables($id, $joined_tables);
374 18
			$this->find_update_files_tags($id, $data, $arguments);
375
		}
376 18
		return true;
377
	}
378
	/**
379
	 * Delete item
380
	 *
381
	 * @param int|int[]|string|string[] $id
382
	 *
383
	 * @return bool
384
	 */
385 20
	protected function delete ($id) {
386
		/** @noinspection ExceptionsAnnotatingAndHandlingInspection */
387 20
		$this->db_prime()->transaction(
388 20
			function () use (&$result, $id) {
389 20
				$result = $this->delete_internal($this->table, $this->data_model, $id);
390 20
			}
391
		);
392 20
		return $result;
393
	}
394
	/**
395
	 * Delete item
396
	 *
397
	 * @param string                    $table
398
	 * @param callable[]|string[]       $data_model
399
	 * @param int|int[]|string|string[] $id
400
	 *
401
	 * @return bool
402
	 */
403 20
	private function delete_internal ($table, $data_model, $id) {
404 20
		$id           = (array)$id;
405 20
		$result       = true;
406 20
		$multilingual = $this->is_multilingual();
407 20
		$first_column = array_keys($data_model)[0];
408 20
		foreach ($id as $i) {
409
			$result =
410 20
				$result &&
411 20
				$this->read_internal($table, $data_model, $i) &&
412 20
				$this->db_prime()->q(
413 20
					"DELETE FROM `$table`
414 20
					WHERE `$first_column` = ?",
415
					$i
416
				);
417
			/**
418
			 * If there are multilingual fields - handle multilingual deleting of fields automatically
419
			 */
420 20
			if ($multilingual) {
421 2
				foreach (array_keys($this->data_model) as $field) {
422 2
					if (is_string($this->data_model[$field]) && strpos($this->data_model[$field], 'ml:') === 0) {
423 2
						Text::instance()->del($this->cdb(), "$this->data_model_ml_group/$field", $i);
424
					}
425
				}
426
			}
427 20
			$this->update_joined_tables($i, []);
428 20
			$this->delete_files_tags($i);
429
		}
430 20
		return $result;
431
	}
432
}
433