Overview

Namespaces

  • Charcoal
    • Object
    • User
      • Acl

Classes

  • Charcoal\Object\Content
  • Charcoal\Object\ObjectRevision
  • Charcoal\Object\ObjectRoute
  • Charcoal\Object\ObjectSchedule
  • Charcoal\Object\UserData
  • Charcoal\User\AbstractUser
  • Charcoal\User\Acl\Manager
  • Charcoal\User\Acl\Permission
  • Charcoal\User\Acl\PermissionCategory
  • Charcoal\User\Acl\Role
  • Charcoal\User\Authenticator
  • Charcoal\User\Authorizer
  • Charcoal\User\AuthToken
  • Charcoal\User\AuthTokenMetadata
  • Charcoal\User\GenericUser

Interfaces

  • Charcoal\Object\ArchivableInterface
  • Charcoal\Object\CategorizableInterface
  • Charcoal\Object\CategorizableMultipleInterface
  • Charcoal\Object\CategoryInterface
  • Charcoal\Object\ContentInterface
  • Charcoal\Object\HierarchicalInterface
  • Charcoal\Object\ObjectRevisionInterface
  • Charcoal\Object\ObjectRouteInterface
  • Charcoal\Object\ObjectScheduleInterface
  • Charcoal\Object\PublishableInterface
  • Charcoal\Object\RevisionableInterface
  • Charcoal\Object\RoutableInterface
  • Charcoal\Object\UserDataInterface
  • Charcoal\User\UserInterface

Traits

  • Charcoal\Object\ArchivableTrait
  • Charcoal\Object\CategorizableMultipleTrait
  • Charcoal\Object\CategorizableTrait
  • Charcoal\Object\CategoryTrait
  • Charcoal\Object\HierarchicalTrait
  • Charcoal\Object\PublishableTrait
  • Charcoal\Object\RevisionableTrait
  • Charcoal\Object\RoutableTrait
  • Overview
  • Namespace
  • Class
  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:  * Represents the changeset of an object.
 25:  *
 26:  * A revision is a record of modifications to an object.
 27:  *
 28:  * Intended to be used to collect all routes related to models
 29:  * under a single source (e.g., database table).
 30:  *
 31:  * {@see Charcoal\Object\ObjectRoute} for a similar model that aggregates data
 32:  * under a common source.
 33:  */
 34: class ObjectRevision extends AbstractModel implements ObjectRevisionInterface
 35: {
 36:     /**
 37:      * Object type of this revision (required)
 38:      * @var string $targetType
 39:      */
 40:     private $targetType;
 41: 
 42:     /**
 43:      * Object ID of this revision (required)
 44:      * @var mixed $objectId
 45:      */
 46:     private $targetId;
 47: 
 48:     /**
 49:      * Revision number. Sequential integer for each object's ID. (required)
 50:      * @var integer $revNum
 51:      */
 52:     private $revNum;
 53: 
 54:     /**
 55:      * Timestamp; when this revision was created
 56:      * @var DateTimeInterface $revTs
 57:      */
 58:     private $revTs;
 59: 
 60:     /**
 61:      * The (admin) user that was
 62:      * @var string $revUser
 63:      */
 64:     private $revUser;
 65: 
 66:     /**
 67:      * @var array $dataPrev
 68:      */
 69:     private $dataPrev;
 70: 
 71:     /**
 72:      * @var array $dataObj
 73:      */
 74:     private $dataObj;
 75: 
 76:     /**
 77:      * @var array $dataDiff
 78:      */
 79:     private $dataDiff;
 80: 
 81:     /**
 82:      * @var FactoryInterface $modelFactory
 83:      */
 84:     private $modelFactory;
 85: 
 86:     /**
 87:      * Dependencies
 88:      * @param Container $container DI Container.
 89:      * @return void
 90:      */
 91:     public function setDependencies(Container $container)
 92:     {
 93:         parent::setDependencies($container);
 94: 
 95:         $this->setModelFactory($container['model/factory']);
 96:     }
 97: 
 98:     /**
 99:      * @param FactoryInterface $factory The factory used to create models.
100:      * @return void
101:      */
102:     protected function setModelFactory(FactoryInterface $factory)
103:     {
104:         $this->modelFactory = $factory;
105:     }
106: 
107:     /**
108:      * @return FactoryInterface The model factory.
109:      */
110:     protected function modelFactory()
111:     {
112:         return $this->modelFactory;
113:     }
114: 
115:     /**
116:      * @param string $targetType The object type (type-ident).
117:      * @throws InvalidArgumentException If the obj type parameter is not a string.
118:      * @return ObjectRevision Chainable
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:      * @return string
133:      */
134:     public function targetType()
135:     {
136:         return $this->targetType;
137:     }
138: 
139:     /**
140:      * @param mixed $targetId The object ID.
141:      * @return ObjectRevision Chainable
142:      */
143:     public function setTargetId($targetId)
144:     {
145:         $this->targetId = $targetId;
146:         return $this;
147:     }
148: 
149:     /**
150:      * @return mixed
151:      */
152:     public function targetId()
153:     {
154:         return $this->targetId;
155:     }
156: 
157:     /**
158:      * @param integer $revNum The revision number.
159:      * @throws InvalidArgumentException If the revision number argument is not numerical.
160:      * @return ObjectRevision Chainable
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:      * @return integer
175:      */
176:     public function revNum()
177:     {
178:         return $this->revNum;
179:     }
180: 
181:     /**
182:      * @param mixed $revTs The revision's timestamp.
183:      * @throws InvalidArgumentException If the timestamp is invalid.
184:      * @return ObjectRevision Chainable
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:      * @return DateTimeInterface|null
206:      */
207:     public function revTs()
208:     {
209:         return $this->revTs;
210:     }
211: 
212:     /**
213:      * @param string $revUser The revision user ident.
214:      * @throws InvalidArgumentException If the revision user parameter is not a string.
215:      * @return ObjectRevision Chainable
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:      * @return string
234:      */
235:     public function revUser()
236:     {
237:         return $this->revUser;
238:     }
239: 
240:     /**
241:      * @param string|array $data The previous revision data.
242:      * @return ObjectRevision Chainable
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:      * @return array
258:      */
259:     public function dataPrev()
260:     {
261:         return $this->dataPrev;
262:     }
263: 
264:     /**
265:      * @param array|string $data The current revision (object) data.
266:      * @return ObjectRevision Chainable
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:      * @return array
282:      */
283:     public function dataObj()
284:     {
285:         return $this->dataObj;
286:     }
287: 
288:     /**
289:      * @param array|string $data The data diff.
290:      * @return ObjectRevision
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:      * @return array
306:      */
307:     public function dataDiff()
308:     {
309:         return $this->dataDiff;
310:     }
311: 
312:     /**
313:      * Create a new revision from an object
314:      *
315:      * 1. Load the last revision
316:      * 2. Load the current item from DB
317:      * 3. Create diff from (1) and (2).
318:      *
319:      * @param RevisionableInterface $obj The object to create the revision from.
320:      * @return ObjectRevision Chainable
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:      * @param array $dataPrev Optional. Previous revision data.
346:      * @param array $dataObj  Optional. Current revision (object) data.
347:      * @return array The diff data
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:      * Recursive arrayDiff.
363:      *
364:      * @param array $array1 First array.
365:      * @param array $array2 Second Array.
366:      * @return array The array diff.
367:      */
368:     public function recursiveDiff(array $array1, array $array2)
369:     {
370:         $diff = [];
371: 
372:         // Compare array1
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:         // Compare array2
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:      * @param RevisionableInterface $obj The object  to load the last revision of.
409:      * @return ObjectRevision The last revision for the give object.
410:      */
411:     public function lastObjectRevision(RevisionableInterface $obj)
412:     {
413:         if ($this->source()->tableExists() === false) {
414:             /** @todo Optionnally turn off for some models */
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:      * Retrieve a specific object revision, by revision number.
444:      *
445:      * @param RevisionableInterface $obj    Target object.
446:      * @param integer               $revNum The revision number to load.
447:      * @return ObjectRevision
448:      */
449:     public function objectRevisionNum(RevisionableInterface $obj, $revNum)
450:     {
451:         if ($this->source()->tableExists() === false) {
452:             /** @todo Optionnally turn off for some models */
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: 
API documentation generated by ApiGen