|
1
|
|
|
<?php |
|
2
|
|
|
|
|
3
|
|
|
namespace Charcoal\Object; |
|
4
|
|
|
|
|
5
|
|
|
// Dependencies from `PHP` |
|
6
|
|
|
use \InvalidArgumentException; |
|
7
|
|
|
use \DateTime; |
|
8
|
|
|
use \DateTimeInterface; |
|
9
|
|
|
|
|
10
|
|
|
// From `pimple/pimple` |
|
11
|
|
|
use \Pimple\Container; |
|
12
|
|
|
|
|
13
|
|
|
// From `charcoal-factory` |
|
14
|
|
|
use \Charcoal\Factory\FactoryInterface; |
|
15
|
|
|
|
|
16
|
|
|
// From `charcoal-core` |
|
17
|
|
|
use \Charcoal\Model\AbstractModel; |
|
18
|
|
|
|
|
19
|
|
|
// Local namespace dependencies |
|
20
|
|
|
use \Charcoal\Object\ObjectRevisionInterface; |
|
21
|
|
|
use \Charcoal\Object\RevisionableInterface; |
|
22
|
|
|
|
|
23
|
|
|
/** |
|
24
|
|
|
* |
|
25
|
|
|
*/ |
|
26
|
|
|
class ObjectRevision extends AbstractModel implements ObjectRevisionInterface |
|
27
|
|
|
{ |
|
28
|
|
|
|
|
29
|
|
|
/** |
|
30
|
|
|
* Object type of this revision (required) |
|
31
|
|
|
* @var string $targetType |
|
32
|
|
|
*/ |
|
33
|
|
|
private $targetType; |
|
34
|
|
|
|
|
35
|
|
|
/** |
|
36
|
|
|
* Object ID of this revision (required) |
|
37
|
|
|
* @var mixed $objectId |
|
38
|
|
|
*/ |
|
39
|
|
|
private $targetId; |
|
40
|
|
|
|
|
41
|
|
|
/** |
|
42
|
|
|
* Revision number. Sequential integer for each object's ID. (required) |
|
43
|
|
|
* @var integer $revNum |
|
44
|
|
|
*/ |
|
45
|
|
|
private $revNum; |
|
46
|
|
|
|
|
47
|
|
|
/** |
|
48
|
|
|
* Timestamp; when this revision was created |
|
49
|
|
|
* @var string $revTs (DateTime) |
|
50
|
|
|
*/ |
|
51
|
|
|
private $revTs; |
|
52
|
|
|
|
|
53
|
|
|
/** |
|
54
|
|
|
* The (admin) user that was |
|
55
|
|
|
* @var string $revUser |
|
56
|
|
|
*/ |
|
57
|
|
|
private $revUser; |
|
58
|
|
|
|
|
59
|
|
|
/** |
|
60
|
|
|
* @var array $dataPrev |
|
61
|
|
|
*/ |
|
62
|
|
|
private $dataPrev; |
|
63
|
|
|
|
|
64
|
|
|
/** |
|
65
|
|
|
* @var array $dataObj |
|
66
|
|
|
*/ |
|
67
|
|
|
private $dataObj; |
|
68
|
|
|
|
|
69
|
|
|
/** |
|
70
|
|
|
* @var array $dataDiff |
|
71
|
|
|
*/ |
|
72
|
|
|
private $dataDiff; |
|
73
|
|
|
|
|
74
|
|
|
/** |
|
75
|
|
|
* @var FactoryInterface $modelFactory |
|
76
|
|
|
*/ |
|
77
|
|
|
private $modelFactory; |
|
78
|
|
|
|
|
79
|
|
|
/** |
|
80
|
|
|
* Dependencies |
|
81
|
|
|
* @param Container $container DI Container. |
|
82
|
|
|
* @return void |
|
83
|
|
|
*/ |
|
84
|
|
|
public function setDependencies(Container $container) |
|
85
|
|
|
{ |
|
86
|
|
|
parent::setDependencies($container); |
|
87
|
|
|
|
|
88
|
|
|
$this->setModelFactory($container['model/factory']); |
|
89
|
|
|
} |
|
90
|
|
|
|
|
91
|
|
|
/** |
|
92
|
|
|
* @param FactoryInterface $factory The factory used to create models. |
|
93
|
|
|
* @return AdminScript Chainable |
|
94
|
|
|
*/ |
|
95
|
|
|
protected function setModelFactory(FactoryInterface $factory) |
|
96
|
|
|
{ |
|
97
|
|
|
$this->modelFactory = $factory; |
|
98
|
|
|
return $this; |
|
99
|
|
|
} |
|
100
|
|
|
|
|
101
|
|
|
/** |
|
102
|
|
|
* @return FactoryInterface The model factory. |
|
103
|
|
|
*/ |
|
104
|
|
|
protected function modelFactory() |
|
105
|
|
|
{ |
|
106
|
|
|
return $this->modelFactory; |
|
107
|
|
|
} |
|
108
|
|
|
|
|
109
|
|
|
/** |
|
110
|
|
|
* @param string $targetType The object type (type-ident). |
|
111
|
|
|
* @throws InvalidArgumentException If the obj type parameter is not a string. |
|
112
|
|
|
* @return ObjectRevision Chainable |
|
113
|
|
|
*/ |
|
114
|
|
|
public function setTargetType($targetType) |
|
115
|
|
|
{ |
|
116
|
|
|
if (!is_string($targetType)) { |
|
117
|
|
|
throw new InvalidArgumentException( |
|
118
|
|
|
'Revisions obj type must be a string.' |
|
119
|
|
|
); |
|
120
|
|
|
} |
|
121
|
|
|
$this->targetType = $targetType; |
|
122
|
|
|
return $this; |
|
123
|
|
|
} |
|
124
|
|
|
|
|
125
|
|
|
/** |
|
126
|
|
|
* @return string |
|
127
|
|
|
*/ |
|
128
|
|
|
public function targetType() |
|
129
|
|
|
{ |
|
130
|
|
|
return $this->targetType; |
|
131
|
|
|
} |
|
132
|
|
|
|
|
133
|
|
|
/** |
|
134
|
|
|
* @param mixed $targetId The object ID. |
|
135
|
|
|
* @return ObjectRevision Chainable |
|
136
|
|
|
*/ |
|
137
|
|
|
public function setTargetId($targetId) |
|
138
|
|
|
{ |
|
139
|
|
|
$this->targetId = $targetId; |
|
140
|
|
|
return $this; |
|
141
|
|
|
} |
|
142
|
|
|
|
|
143
|
|
|
/** |
|
144
|
|
|
* @return mixed |
|
145
|
|
|
*/ |
|
146
|
|
|
public function targetId() |
|
147
|
|
|
{ |
|
148
|
|
|
return $this->targetId; |
|
149
|
|
|
} |
|
150
|
|
|
|
|
151
|
|
|
/** |
|
152
|
|
|
* @param integer $revNum The revision number. |
|
153
|
|
|
* @throws InvalidArgumentException If the revision number argument is not numerical. |
|
154
|
|
|
* @return ObjectRevision Chainable |
|
155
|
|
|
*/ |
|
156
|
|
|
public function setRevNum($revNum) |
|
157
|
|
|
{ |
|
158
|
|
|
if (!is_numeric($revNum)) { |
|
159
|
|
|
throw new InvalidArgumentException( |
|
160
|
|
|
'Revision number must be an integer (numeric).' |
|
161
|
|
|
); |
|
162
|
|
|
} |
|
163
|
|
|
$this->revNum = (int)$revNum; |
|
164
|
|
|
return $this; |
|
165
|
|
|
} |
|
166
|
|
|
|
|
167
|
|
|
/** |
|
168
|
|
|
* @return integer |
|
169
|
|
|
*/ |
|
170
|
|
|
public function revNum() |
|
171
|
|
|
{ |
|
172
|
|
|
return $this->revNum; |
|
173
|
|
|
} |
|
174
|
|
|
|
|
175
|
|
|
/** |
|
176
|
|
|
* @param mixed $revTs The revision's timestamp. |
|
177
|
|
|
* @throws InvalidArgumentException If the timestamp is invalid. |
|
178
|
|
|
* @return ObjectRevision Chainable |
|
179
|
|
|
*/ |
|
180
|
|
View Code Duplication |
public function setRevTs($revTs) |
|
|
|
|
|
|
181
|
|
|
{ |
|
182
|
|
|
if ($revTs === null) { |
|
183
|
|
|
$this->revTs = null; |
|
184
|
|
|
return $this; |
|
185
|
|
|
} |
|
186
|
|
|
if (is_string($revTs)) { |
|
187
|
|
|
$revTs = new DateTime($revTs); |
|
188
|
|
|
} |
|
189
|
|
|
if (!($revTs instanceof DateTimeInterface)) { |
|
190
|
|
|
throw new InvalidArgumentException( |
|
191
|
|
|
'Invalid "Revision Date" value. Must be a date/time string or a DateTimeInterface object.' |
|
192
|
|
|
); |
|
193
|
|
|
} |
|
194
|
|
|
$this->revTs = $revTs; |
|
|
|
|
|
|
195
|
|
|
return $this; |
|
196
|
|
|
} |
|
197
|
|
|
|
|
198
|
|
|
/** |
|
199
|
|
|
* @return DateTime|null |
|
200
|
|
|
*/ |
|
201
|
|
|
public function revTs() |
|
202
|
|
|
{ |
|
203
|
|
|
return $this->revTs; |
|
|
|
|
|
|
204
|
|
|
} |
|
205
|
|
|
|
|
206
|
|
|
/** |
|
207
|
|
|
* @param string $revUser The revision user ident. |
|
208
|
|
|
* @throws InvalidArgumentException If the revision user parameter is not a string. |
|
209
|
|
|
* @return ObjectRevision Chainable |
|
210
|
|
|
*/ |
|
211
|
|
|
public function setRevUser($revUser) |
|
212
|
|
|
{ |
|
213
|
|
|
if ($revUser === null) { |
|
214
|
|
|
$this->revUser = null; |
|
215
|
|
|
return $this; |
|
216
|
|
|
} |
|
217
|
|
|
if (!is_string($revUser)) { |
|
218
|
|
|
throw new InvalidArgumentException( |
|
219
|
|
|
'Revision user must be a string.' |
|
220
|
|
|
); |
|
221
|
|
|
} |
|
222
|
|
|
$this->revUser = $revUser; |
|
223
|
|
|
return $this; |
|
224
|
|
|
} |
|
225
|
|
|
|
|
226
|
|
|
/** |
|
227
|
|
|
* @return string |
|
228
|
|
|
*/ |
|
229
|
|
|
public function revUser() |
|
230
|
|
|
{ |
|
231
|
|
|
return $this->revUser; |
|
232
|
|
|
} |
|
233
|
|
|
|
|
234
|
|
|
/** |
|
235
|
|
|
* @param string|array $data The previous revision data. |
|
236
|
|
|
* @return ObjectRevision |
|
237
|
|
|
*/ |
|
238
|
|
View Code Duplication |
public function setDataPrev($data) |
|
|
|
|
|
|
239
|
|
|
{ |
|
240
|
|
|
if (!is_array($data)) { |
|
241
|
|
|
$data = json_decode($data, true); |
|
242
|
|
|
} |
|
243
|
|
|
if ($data === null) { |
|
244
|
|
|
$data = []; |
|
245
|
|
|
} |
|
246
|
|
|
$this->dataPrev = $data; |
|
|
|
|
|
|
247
|
|
|
return $this; |
|
248
|
|
|
} |
|
249
|
|
|
|
|
250
|
|
|
/** |
|
251
|
|
|
* @return array |
|
252
|
|
|
*/ |
|
253
|
|
|
public function dataPrev() |
|
254
|
|
|
{ |
|
255
|
|
|
return $this->dataPrev; |
|
256
|
|
|
} |
|
257
|
|
|
|
|
258
|
|
|
/** |
|
259
|
|
|
* @param array|string $data The current revision (object) data. |
|
260
|
|
|
* @return ObjectRevision |
|
261
|
|
|
*/ |
|
262
|
|
View Code Duplication |
public function setDataObj($data) |
|
|
|
|
|
|
263
|
|
|
{ |
|
264
|
|
|
if (!is_array($data)) { |
|
265
|
|
|
$data = json_decode($data, true); |
|
266
|
|
|
} |
|
267
|
|
|
if ($data === null) { |
|
268
|
|
|
$data = []; |
|
269
|
|
|
} |
|
270
|
|
|
$this->dataObj = $data; |
|
|
|
|
|
|
271
|
|
|
return $this; |
|
272
|
|
|
} |
|
273
|
|
|
|
|
274
|
|
|
/** |
|
275
|
|
|
* @return array |
|
276
|
|
|
*/ |
|
277
|
|
|
public function dataObj() |
|
278
|
|
|
{ |
|
279
|
|
|
return $this->dataObj; |
|
280
|
|
|
} |
|
281
|
|
|
|
|
282
|
|
|
/** |
|
283
|
|
|
* @param array|string $data The data diff. |
|
284
|
|
|
* @return ObjectRevision |
|
285
|
|
|
*/ |
|
286
|
|
View Code Duplication |
public function setDataDiff($data) |
|
|
|
|
|
|
287
|
|
|
{ |
|
288
|
|
|
if (!is_array($data)) { |
|
289
|
|
|
$data = json_decode($data, true); |
|
290
|
|
|
} |
|
291
|
|
|
if ($data === null) { |
|
292
|
|
|
$data = []; |
|
293
|
|
|
} |
|
294
|
|
|
$this->dataDiff = $data; |
|
|
|
|
|
|
295
|
|
|
return $this; |
|
296
|
|
|
} |
|
297
|
|
|
|
|
298
|
|
|
/** |
|
299
|
|
|
* @return array |
|
300
|
|
|
*/ |
|
301
|
|
|
public function dataDiff() |
|
302
|
|
|
{ |
|
303
|
|
|
return $this->dataDiff; |
|
304
|
|
|
} |
|
305
|
|
|
|
|
306
|
|
|
/** |
|
307
|
|
|
* Create a new revision from an object |
|
308
|
|
|
* |
|
309
|
|
|
* 1. Load the last revision |
|
310
|
|
|
* 2. Load the current item from DB |
|
311
|
|
|
* 3. Create diff from (1) and (2). |
|
312
|
|
|
* |
|
313
|
|
|
* @param RevisionableInterface $obj The object to create the revision from. |
|
314
|
|
|
* @return ObjectRevision Chainable |
|
315
|
|
|
*/ |
|
316
|
|
|
public function createFromObject(RevisionableInterface $obj) |
|
317
|
|
|
{ |
|
318
|
|
|
$prevRev = $this->lastObjectRevision($obj); |
|
319
|
|
|
|
|
320
|
|
|
$this->setObjType($obj->targetType()); |
|
|
|
|
|
|
321
|
|
|
$this->setObjId($obj->id()); |
|
|
|
|
|
|
322
|
|
|
$this->setRevNum($prevRev->revNum() + 1); |
|
323
|
|
|
$this->setRevTs('now'); |
|
324
|
|
|
|
|
325
|
|
|
$this->setDataObj($obj->data([ |
|
326
|
|
|
'sortable'=>false |
|
327
|
|
|
])); |
|
328
|
|
|
$this->setDataPrev($prevRev->dataObj()); |
|
329
|
|
|
|
|
330
|
|
|
$diff = $this->createDiff(); |
|
331
|
|
|
$this->setDataDiff($diff); |
|
332
|
|
|
|
|
333
|
|
|
return $this; |
|
334
|
|
|
} |
|
335
|
|
|
|
|
336
|
|
|
/** |
|
337
|
|
|
* @param array $dataPrev Optional. Previous revision data. |
|
338
|
|
|
* @param array $dataObj Optional. Current revision (object) data. |
|
339
|
|
|
* @return array The diff data |
|
340
|
|
|
*/ |
|
341
|
|
|
public function createDiff(array $dataPrev = null, array $dataObj = null) |
|
342
|
|
|
{ |
|
343
|
|
|
if ($dataPrev === null) { |
|
344
|
|
|
$dataPrev = $this->dataPrev(); |
|
345
|
|
|
} |
|
346
|
|
|
if ($dataObj === null) { |
|
347
|
|
|
$dataObj = $this->dataObj(); |
|
348
|
|
|
} |
|
349
|
|
|
$dataDiff = $this->recursiveDiff($dataPrev, $dataObj); |
|
350
|
|
|
return $dataDiff; |
|
351
|
|
|
} |
|
352
|
|
|
|
|
353
|
|
|
/** |
|
354
|
|
|
* Recursive arrayDiff. |
|
355
|
|
|
* |
|
356
|
|
|
* @param array $array1 First array. |
|
357
|
|
|
* @param array $array2 Second Array. |
|
358
|
|
|
* @return array The array diff. |
|
359
|
|
|
*/ |
|
360
|
|
|
public function recursiveDiff(array $array1, array $array2) |
|
361
|
|
|
{ |
|
362
|
|
|
$diff = []; |
|
363
|
|
|
|
|
364
|
|
|
// Compare array1 |
|
365
|
|
|
foreach ($array1 as $key => $value) { |
|
366
|
|
|
if (!array_key_exists($key, $array2)) { |
|
367
|
|
|
$diff[0][$key] = $value; |
|
368
|
|
|
} elseif (is_array($value)) { |
|
369
|
|
|
if (!is_array($array2[$key])) { |
|
370
|
|
|
$diff[0][$key] = $value; |
|
371
|
|
|
$diff[1][$key] = $array2[$key]; |
|
372
|
|
|
} else { |
|
373
|
|
|
$new = $this->recursiveDiff($value, $array2[$key]); |
|
374
|
|
|
if ($new !== false) { |
|
375
|
|
|
if (isset($new[0])) { |
|
376
|
|
|
$diff[0][$key] = $new[0]; |
|
377
|
|
|
} |
|
378
|
|
|
if (isset($new[1])) { |
|
379
|
|
|
$diff[1][$key] = $new[1]; |
|
380
|
|
|
} |
|
381
|
|
|
} |
|
382
|
|
|
} |
|
383
|
|
|
} elseif ($array2[$key] !== $value) { |
|
384
|
|
|
$diff[0][$key] = $value; |
|
385
|
|
|
$diff[1][$key] = $array2[$key]; |
|
386
|
|
|
} |
|
387
|
|
|
} |
|
388
|
|
|
|
|
389
|
|
|
// Compare array2 |
|
390
|
|
|
foreach ($array2 as $key => $value) { |
|
391
|
|
|
if (!array_key_exists($key, $array1)) { |
|
392
|
|
|
$diff[1][$key] = $value; |
|
393
|
|
|
} |
|
394
|
|
|
} |
|
395
|
|
|
|
|
396
|
|
|
return $diff; |
|
397
|
|
|
} |
|
398
|
|
|
|
|
399
|
|
|
/** |
|
400
|
|
|
* @param RevisionableInterface $obj The object to load the last revision of. |
|
401
|
|
|
* @return ObjectRevision The last revision for the give object. |
|
402
|
|
|
*/ |
|
403
|
|
|
public function lastObjectRevision(RevisionableInterface $obj) |
|
404
|
|
|
{ |
|
405
|
|
|
if ($this->source()->tableExists() === false) { |
|
|
|
|
|
|
406
|
|
|
/** @todo Optionnally turn off for some models */ |
|
407
|
|
|
$this->source()->createTable(); |
|
|
|
|
|
|
408
|
|
|
} |
|
409
|
|
|
|
|
410
|
|
|
$rev = $this->modelFactory()->create(self::class); |
|
411
|
|
|
|
|
412
|
|
|
$rev->loadFromQuery( |
|
413
|
|
|
' |
|
414
|
|
|
SELECT |
|
415
|
|
|
* |
|
416
|
|
|
FROM |
|
417
|
|
|
`'.$this->source()->table().'` |
|
|
|
|
|
|
418
|
|
|
WHERE |
|
419
|
|
|
`target_type` = :target_type |
|
420
|
|
|
AND |
|
421
|
|
|
`target_id` = :target_id |
|
422
|
|
|
ORDER BY |
|
423
|
|
|
`rev_ts` desc |
|
424
|
|
|
LIMIT 1', |
|
425
|
|
|
[ |
|
426
|
|
|
'target_type' => $obj->targetType(), |
|
|
|
|
|
|
427
|
|
|
'target_id' => $obj->id() |
|
428
|
|
|
] |
|
429
|
|
|
); |
|
430
|
|
|
|
|
431
|
|
|
return $rev; |
|
432
|
|
|
} |
|
433
|
|
|
} |
|
434
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.