1: <?php
2:
3: namespace Charcoal\Object;
4:
5:
6: use \InvalidArgumentException;
7: use \DateTime;
8: use \DateTimeInterface;
9:
10:
11: use \Pimple\Container;
12:
13:
14: use \Charcoal\Factory\FactoryInterface;
15:
16:
17: use \Charcoal\Model\AbstractModel;
18:
19:
20: use \Charcoal\Object\ObjectRevisionInterface;
21: use \Charcoal\Object\RevisionableInterface;
22:
23: 24: 25: 26: 27: 28: 29: 30: 31: 32: 33:
34: class ObjectRevision extends AbstractModel implements ObjectRevisionInterface
35: {
36: 37: 38: 39:
40: private $targetType;
41:
42: 43: 44: 45:
46: private $targetId;
47:
48: 49: 50: 51:
52: private $revNum;
53:
54: 55: 56: 57:
58: private $revTs;
59:
60: 61: 62: 63:
64: private $revUser;
65:
66: 67: 68:
69: private $dataPrev;
70:
71: 72: 73:
74: private $dataObj;
75:
76: 77: 78:
79: private $dataDiff;
80:
81: 82: 83:
84: private $modelFactory;
85:
86: 87: 88: 89: 90:
91: public function setDependencies(Container $container)
92: {
93: parent::setDependencies($container);
94:
95: $this->setModelFactory($container['model/factory']);
96: }
97:
98: 99: 100: 101:
102: protected function setModelFactory(FactoryInterface $factory)
103: {
104: $this->modelFactory = $factory;
105: }
106:
107: 108: 109:
110: protected function modelFactory()
111: {
112: return $this->modelFactory;
113: }
114:
115: 116: 117: 118: 119:
120: public function setTargetType($targetType)
121: {
122: if (!is_string($targetType)) {
123: throw new InvalidArgumentException(
124: 'Revisions obj type must be a string.'
125: );
126: }
127: $this->targetType = $targetType;
128: return $this;
129: }
130:
131: 132: 133:
134: public function targetType()
135: {
136: return $this->targetType;
137: }
138:
139: 140: 141: 142:
143: public function setTargetId($targetId)
144: {
145: $this->targetId = $targetId;
146: return $this;
147: }
148:
149: 150: 151:
152: public function targetId()
153: {
154: return $this->targetId;
155: }
156:
157: 158: 159: 160: 161:
162: public function setRevNum($revNum)
163: {
164: if (!is_numeric($revNum)) {
165: throw new InvalidArgumentException(
166: 'Revision number must be an integer (numeric).'
167: );
168: }
169: $this->revNum = (int)$revNum;
170: return $this;
171: }
172:
173: 174: 175:
176: public function revNum()
177: {
178: return $this->revNum;
179: }
180:
181: 182: 183: 184: 185:
186: public function setRevTs($revTs)
187: {
188: if ($revTs === null) {
189: $this->revTs = null;
190: return $this;
191: }
192: if (is_string($revTs)) {
193: $revTs = new DateTime($revTs);
194: }
195: if (!($revTs instanceof DateTimeInterface)) {
196: throw new InvalidArgumentException(
197: 'Invalid "Revision Date" value. Must be a date/time string or a DateTimeInterface object.'
198: );
199: }
200: $this->revTs = $revTs;
201: return $this;
202: }
203:
204: 205: 206:
207: public function revTs()
208: {
209: return $this->revTs;
210: }
211:
212: 213: 214: 215: 216:
217: public function setRevUser($revUser)
218: {
219: if ($revUser === null) {
220: $this->revUser = null;
221: return $this;
222: }
223: if (!is_string($revUser)) {
224: throw new InvalidArgumentException(
225: 'Revision user must be a string.'
226: );
227: }
228: $this->revUser = $revUser;
229: return $this;
230: }
231:
232: 233: 234:
235: public function revUser()
236: {
237: return $this->revUser;
238: }
239:
240: 241: 242: 243:
244: public function setDataPrev($data)
245: {
246: if (!is_array($data)) {
247: $data = json_decode($data, true);
248: }
249: if ($data === null) {
250: $data = [];
251: }
252: $this->dataPrev = $data;
253: return $this;
254: }
255:
256: 257: 258:
259: public function dataPrev()
260: {
261: return $this->dataPrev;
262: }
263:
264: 265: 266: 267:
268: public function setDataObj($data)
269: {
270: if (!is_array($data)) {
271: $data = json_decode($data, true);
272: }
273: if ($data === null) {
274: $data = [];
275: }
276: $this->dataObj = $data;
277: return $this;
278: }
279:
280: 281: 282:
283: public function dataObj()
284: {
285: return $this->dataObj;
286: }
287:
288: 289: 290: 291:
292: public function setDataDiff($data)
293: {
294: if (!is_array($data)) {
295: $data = json_decode($data, true);
296: }
297: if ($data === null) {
298: $data = [];
299: }
300: $this->dataDiff = $data;
301: return $this;
302: }
303:
304: 305: 306:
307: public function dataDiff()
308: {
309: return $this->dataDiff;
310: }
311:
312: 313: 314: 315: 316: 317: 318: 319: 320: 321:
322: public function createFromObject(RevisionableInterface $obj)
323: {
324: $prevRev = $this->lastObjectRevision($obj);
325:
326: $this->setTargetType($obj->objType());
327: $this->setTargetId($obj->id());
328: $this->setRevNum($prevRev->revNum() + 1);
329: $this->setRevTs('now');
330:
331: if (is_callable([$obj, 'lastModifiedBy'])) {
332: $this->setRevUser($obj->lastModifiedBy());
333: }
334:
335: $this->setDataObj($obj->data());
336: $this->setDataPrev($prevRev->dataObj());
337:
338: $diff = $this->createDiff();
339: $this->setDataDiff($diff);
340:
341: return $this;
342: }
343:
344: 345: 346: 347: 348:
349: public function createDiff(array $dataPrev = null, array $dataObj = null)
350: {
351: if ($dataPrev === null) {
352: $dataPrev = $this->dataPrev();
353: }
354: if ($dataObj === null) {
355: $dataObj = $this->dataObj();
356: }
357: $dataDiff = $this->recursiveDiff($dataPrev, $dataObj);
358: return $dataDiff;
359: }
360:
361: 362: 363: 364: 365: 366: 367:
368: public function recursiveDiff(array $array1, array $array2)
369: {
370: $diff = [];
371:
372:
373: foreach ($array1 as $key => $value) {
374: if (!array_key_exists($key, $array2)) {
375: $diff[0][$key] = $value;
376: } elseif (is_array($value)) {
377: if (!is_array($array2[$key])) {
378: $diff[0][$key] = $value;
379: $diff[1][$key] = $array2[$key];
380: } else {
381: $new = $this->recursiveDiff($value, $array2[$key]);
382: if ($new !== false) {
383: if (isset($new[0])) {
384: $diff[0][$key] = $new[0];
385: }
386: if (isset($new[1])) {
387: $diff[1][$key] = $new[1];
388: }
389: }
390: }
391: } elseif ($array2[$key] !== $value) {
392: $diff[0][$key] = $value;
393: $diff[1][$key] = $array2[$key];
394: }
395: }
396:
397:
398: foreach ($array2 as $key => $value) {
399: if (!array_key_exists($key, $array1)) {
400: $diff[1][$key] = $value;
401: }
402: }
403:
404: return $diff;
405: }
406:
407: 408: 409: 410:
411: public function lastObjectRevision(RevisionableInterface $obj)
412: {
413: if ($this->source()->tableExists() === false) {
414:
415: $this->source()->createTable();
416: }
417:
418: $rev = $this->modelFactory()->create(self::class);
419:
420: $rev->loadFromQuery(
421: '
422: SELECT
423: *
424: FROM
425: `'.$this->source()->table().'`
426: WHERE
427: `target_type` = :target_type
428: AND
429: `target_id` = :target_id
430: ORDER BY
431: `rev_ts` desc
432: LIMIT 1',
433: [
434: 'target_type' => $obj->objType(),
435: 'target_id' => $obj->id()
436: ]
437: );
438:
439: return $rev;
440: }
441:
442: 443: 444: 445: 446: 447: 448:
449: public function objectRevisionNum(RevisionableInterface $obj, $revNum)
450: {
451: if ($this->source()->tableExists() === false) {
452:
453: $this->source()->createTable();
454: }
455:
456: $revNum = (int)$revNum;
457:
458: $rev = $this->modelFactory()->create(self::class);
459:
460: $rev->loadFromQuery(
461: '
462: SELECT
463: *
464: FROM
465: `'.$this->source()->table().'`
466: WHERE
467: `target_type` = :target_type
468: AND
469: `target_id` = :target_id
470: AND
471: `rev_num` = :rev_num
472: LIMIT 1',
473: [
474: 'target_type' => $obj->objType(),
475: 'target_id' => $obj->id(),
476: 'rev_num' => $revNum
477: ]
478: );
479:
480: return $rev;
481: }
482: }
483: