Completed
Push — master ( 574567...ef1817 )
by Ron
02:25
created

DiffStorageStore::formatNewRow()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
namespace DataDiff;
3
4
use Generator;
5
use PDO;
6
use PDOStatement;
7
use Traversable;
8
9
class DiffStorageStore implements DiffStorageStoreInterface {
10
	/** @var PDO */
11
	private $pdo;
12
	/** @var PDOStatement */
13
	private $insertStmt;
14
	/** @var PDOStatement */
15
	private $replaceStmt;
16
	/** @var PDOStatement */
17
	private $selectStmt;
18
	/** @var PDOStatement */
19
	private $updateStmt;
20
	/** @var string */
21
	private $storeA;
22
	/** @var string */
23
	private $storeB;
24
	/** @var int */
25
	private $counter = 0;
26
	/** @var callable */
27
	private $duplicateKeyHandler;
28
	/** @var array */
29
	private $converter;
30
	/** @var string[] */
31
	private $keys;
32
	/** @var string[] */
33
	private $valueKeys;
34
35
	/**
36
	 * @param PDO $pdo
37
	 * @param string $keySchema
38
	 * @param string $valueSchema
39
	 * @param string[] $keys
40
	 * @param string[] $valueKeys
41
	 * @param array $converter
42
	 * @param string $storeA
43
	 * @param string $storeB
44
	 * @param callable $duplicateKeyHandler
45
	 */
46
	public function __construct(PDO $pdo, $keySchema, $valueSchema, array $keys, array $valueKeys, array $converter, $storeA, $storeB, $duplicateKeyHandler) {
47
		$this->pdo = $pdo;
48
		$this->selectStmt = $this->pdo->prepare("SELECT s_data FROM data_store WHERE s_ab='{$storeA}' AND s_key={$keySchema} AND (1=1 OR s_value={$valueSchema})");
49
		$this->replaceStmt = $this->pdo->prepare("INSERT OR REPLACE INTO data_store (s_ab, s_key, s_value, s_data, s_sort) VALUES ('{$storeA}', {$keySchema}, {$valueSchema}, :___data, :___sort)");
50
		$this->insertStmt = $this->pdo->prepare("INSERT INTO data_store (s_ab, s_key, s_value, s_data, s_sort) VALUES ('{$storeA}', {$keySchema}, {$valueSchema}, :___data, :___sort)");
51
		$this->updateStmt = $this->pdo->prepare("UPDATE data_store SET s_value={$valueSchema}, s_data=:___data WHERE s_ab='{$storeA}' AND s_key={$keySchema}");
52
		$this->storeA = $storeA;
53
		$this->storeB = $storeB;
54
		$this->keys = $keys;
55
		$this->valueKeys = $valueKeys;
56
		$this->converter = $converter;
57
		$this->duplicateKeyHandler = $duplicateKeyHandler;
58
	}
59
60
	/**
61
	 * @param array $data
62
	 * @param array $translation
63
	 * @param callable $duplicateKeyHandler
64
	 */
65
	public function addRow(array $data, array $translation = null, $duplicateKeyHandler = null) {
66
		$data = $this->translate($data, $translation);
67
		if($duplicateKeyHandler === null) {
68
			$duplicateKeyHandler = $this->duplicateKeyHandler;
69
		}
70
		$buildMetaData = function (array $data, array $keys) {
71
			$metaData = $data;
72
			$metaData = array_diff_key($metaData, array_diff_key($metaData, $keys));
73
			$metaData['___data'] = json_encode($data);
74
			$metaData['___sort'] = $this->counter;
75
			return $metaData;
76
		};
77
		$metaData = $buildMetaData($data, $this->converter);
78
		if($duplicateKeyHandler === null) {
79
			$this->replaceStmt->execute($metaData);
80
		} else {
81
			try {
82
				$this->insertStmt->execute($metaData);
83
			} catch (\PDOException $e) {
84
				if(strpos($e->getMessage(), 'UNIQUE constraint failed') !== false) {
85
					$metaData = $buildMetaData($data, $this->converter);
86
					unset($metaData['___data']);
87
					unset($metaData['___sort']);
88
					$this->selectStmt->execute($metaData);
89
					$oldData = $this->selectStmt->fetch(PDO::FETCH_COLUMN, 0);
90
					if($oldData === null) {
91
						$oldData = [];
92
					} else {
93
						$oldData = json_decode($oldData, true);
94
					}
95
					$data = $duplicateKeyHandler($data, $oldData);
96
					$metaData = $buildMetaData($data, $this->converter);
97
					unset($metaData['___sort']);
98
					$this->updateStmt->execute($metaData);
99
				} else {
100
					throw $e;
101
				}
102
			}
103
		}
104
	}
105
106
	/**
107
	 * @param Traversable|array $rows
108
	 * @param array $translation
109
	 * @param callable $duplicateKeyHandler
110
	 * @return $this
111
	 */
112
	public function addRows($rows, array $translation = null, $duplicateKeyHandler = null) {
113
		foreach($rows as $row) {
114
			$this->addRow($row, $translation, $duplicateKeyHandler);
115
		}
116
		return $this;
117
	}
118
119
	/**
120
	 * Returns true whenever there is any changed, added or removed data available
121
	 */
122
	public function hasAnyChanges() {
123
		/** @noinspection PhpUnusedLocalVariableInspection */
124
		foreach($this->getNewOrChanged() as $_) {
125
			return true;
126
		}
127
		/** @noinspection PhpUnusedLocalVariableInspection */
128
		foreach($this->getMissing() as $_) {
129
			return true;
130
		}
131
		return false;
132
	}
133
134
	/**
135
	 * Get all rows, that are present in this store, but not in the other
136
	 *
137
	 * @return Generator|DiffStorageStoreRow[]
138
	 */
139
	public function getNew() {
140
		return $this->query('
141
			SELECT
142
				s1.s_key AS k,
143
				s1.s_data AS d,
144
				s2.s_data AS f
145
			FROM
146
				data_store AS s1
147
			LEFT JOIN
148
				data_store AS s2 ON s2.s_ab = :sB AND s1.s_key = s2.s_key
149
			WHERE
150
				s1.s_ab = :sA
151
				AND
152
				s2.s_ab IS NULL
153
			ORDER BY
154
				s1.s_sort
155
		', function (DiffStorageStoreRowInterface $row) {
156
			return $this->formatNewRow($row);
157
		});
158
	}
159
160
	/**
161
	 * Get all rows, that have a different value hash in the other store
162
	 *
163
	 * @return Generator|DiffStorageStoreRow[]
164
	 */
165
	public function getChanged() {
166
		return $this->query('
167
			SELECT
168
				s1.s_key AS k,
169
				s1.s_data AS d,
170
				s2.s_data AS f
171
			FROM
172
				data_store AS s1
173
			INNER JOIN
174
				data_store AS s2 ON s2.s_ab = :sB AND s1.s_key = s2.s_key
175
			WHERE
176
				s1.s_ab = :sA
177
				AND
178
				s1.s_value != s2.s_value
179
			ORDER BY
180
				s1.s_sort
181
		', function (DiffStorageStoreRowInterface $row) {
182
			return $this->formatChangedRow($row);
183
		});
184
	}
185
186
	/**
187
	 * @return Generator|DiffStorageStoreRow[]
188
	 */
189
	public function getNewOrChanged() {
190
		return $this->query('
191
			SELECT
192
				s1.s_key AS k,
193
				s1.s_data AS d,
194
				s2.s_data AS f
195
			FROM
196
				data_store AS s1
197
			LEFT JOIN
198
				data_store AS s2 ON s2.s_ab = :sB AND s1.s_key = s2.s_key
199
			WHERE
200
				s1.s_ab = :sA
201
				AND
202
				((s2.s_ab IS NULL) OR (s1.s_value != s2.s_value))
203
			ORDER BY
204
				s1.s_sort
205
		', function (DiffStorageStoreRowInterface $row) {
206
			if(count($row->getForeign()->getValueData())) {
207
				return $this->formatChangedRow($row);
208
			} else {
209
				return $this->formatNewRow($row);
210
			}
211
		});
212
	}
213
214
	/**
215
	 * Get all rows, that are present in the other store, but not in this
216
	 *
217
	 * @return Generator|DiffStorageStoreRow[]
218
	 */
219
	public function getMissing() {
220
		return $this->query('
221
			SELECT
222
				s1.s_key AS k,
223
				s2.s_data AS d,
224
				s1.s_data AS f
225
			FROM
226
				data_store AS s1
227
			LEFT JOIN
228
				data_store AS s2 ON s2.s_ab = :sA AND s2.s_key = s1.s_key
229
			WHERE
230
				s1.s_ab = :sB
231
				AND
232
				s2.s_ab IS NULL
233
			ORDER BY
234
				s1.s_sort
235
		', function (DiffStorageStoreRowInterface $row) {
236
			return $this->formatMissingRow($row);
237
		});
238
	}
239
240
	/**
241
	 * @return $this
242
	 */
243
	public function clearAll() {
244
		$stmt = $this->pdo->query('DELETE FROM data_store WHERE s_ab=:s');
245
		$stmt->execute(['s' => $this->storeA]);
246
		$stmt->closeCursor();
247
	}
248
249
	/**
250
	 * @param string $query
251
	 * @param callable $stringFormatter
252
	 * @return DiffStorageStoreRow[]|Generator
253
	 */
254
	private function query($query, $stringFormatter) {
255
		$stmt = $this->pdo->query($query);
256
		$stmt->execute(['sA' => $this->storeA, 'sB' => $this->storeB]);
257
		while($row = $stmt->fetch(PDO::FETCH_NUM)) {
258
			$d = json_decode($row[1], true);
259
			$f = json_decode($row[2], true);
260
			yield $this->instantiateRow($d, $f, $stringFormatter);
261
		}
262
		$stmt->closeCursor();
263
	}
264
265
	/**
266
	 * @return Traversable|array[]
267
	 */
268
	public function getIterator() {
269
		$query = '
270
			SELECT
271
				s1.s_data AS d
272
			FROM
273
				data_store AS s1
274
			WHERE
275
				s1.s_ab = :s
276
			ORDER BY
277
				s1.s_sort
278
		';
279
		$stmt = $this->pdo->query($query);
280
		$stmt->execute(['s' => $this->storeA]);
281
		while($row = $stmt->fetch(PDO::FETCH_NUM)) {
282
			$row = json_decode($row[0], true);
283
			$row = $this->instantiateRow($row, [], function (DiffStorageStoreRowInterface $row) {
284
				return $this->formatKeyValuePairs($row->getData());
285
			});
286
			yield $row->getData();
287
		}
288
		$stmt->closeCursor();
289
	}
290
291
	/**
292
	 * @param array $data
293
	 * @param array $translation
294
	 * @return array
295
	 */
296
	private function translate(array $data, array $translation = null) {
297
		if($translation !== null) {
298
			$result = [];
299
			foreach($data as $key => $value) {
300
				if(array_key_exists($key, $translation)) {
301
					$key = $translation[$key];
302
				}
303
				$result[$key] = $value;
304
			}
305
			return $result;
306
		}
307
		return $data;
308
	}
309
310
	/**
311
	 * @return int
312
	 */
313
	public function count() {
314
		$query = '
315
			SELECT
316
				COUNT(*)
317
			FROM
318
				data_store AS s1
319
			WHERE
320
				s1.s_ab = :s
321
		';
322
		$stmt = $this->pdo->query($query);
323
		$stmt->execute(['s' => $this->storeA]);
324
		$count = $stmt->fetch(PDO::FETCH_COLUMN, 0);
325
		return $count;
326
	}
327
328
	/**
329
	 * @param array $localData
330
	 * @param array $foreignData
331
	 * @param callable $stringFormatter
332
	 * @return DiffStorageStoreRow
333
	 */
334
	private function instantiateRow(array $localData = null, array $foreignData = null, $stringFormatter) {
335
		return new DiffStorageStoreRow($localData, $foreignData, $this->keys, $this->valueKeys, $this->converter, $stringFormatter);
336
	}
337
338
	/**
339
	 * @param DiffStorageStoreRowInterface $row
340
	 * @return string
341
	 */
342
	private function formatNewRow(DiffStorageStoreRowInterface $row) {
343
		$keys = $this->formatKeyValuePairs($row->getLocal()->getKeyData());
344
		$values = $this->formatKeyValuePairs($row->getLocal()->getValueData());
345
		return sprintf("New %s (%s)", $keys, $values);
346
	}
347
348
	/**
349
	 * @param DiffStorageStoreRowInterface $row
350
	 * @return string
351
	 */
352
	private function formatChangedRow(DiffStorageStoreRowInterface $row) {
353
		$keys = $this->formatKeyValuePairs($row->getLocal()->getKeyData());
354
		$values = $row->getForeign()->getValueData();
355
		$valueKeys = array_keys($values);
356
		return sprintf("Changed %s => %s", $keys, $row->getDiffFormatted($valueKeys));
357
	}
358
359
	/**
360
	 * @param DiffStorageStoreRowInterface $row
361
	 * @return string
362
	 */
363
	private function formatMissingRow(DiffStorageStoreRowInterface $row) {
364
		$keys = $this->formatKeyValuePairs($row->getForeign()->getKeyData());
365
		$values = $this->formatKeyValuePairs($row->getForeign()->getValueData());
366
		return sprintf("Missing %s (%s)", $keys, $values);
367
	}
368
369
	/**
370
	 * @param array $keyValues
371
	 * @return string
372
	 */
373
	private function formatKeyValuePairs($keyValues) {
374
		$keyParts = [];
375
		foreach($keyValues as $key => $value) {
376
			$value = preg_replace('/\\s+/', ' ', $value);
377
			if(strlen($value) > 20) {
378
				$value = substr($value, 0, 16) . ' ...';
379
			}
380
			$keyParts[] = sprintf("%s: %s", $key, json_encode($value, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
381
		}
382
		return join(', ', $keyParts);
383
	}
384
}
385