Completed
Push — master ( 51b910...e719dd )
by Joschi
03:23
created

AbstractObject::hasBeenUndeleted()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 1
eloc 2
c 1
b 0
f 1
nc 1
nop 0
dl 0
loc 4
ccs 2
cts 2
cp 1
crap 1
rs 10
1
<?php
2
3
/**
4
 * apparat-object
5
 *
6
 * @category    Apparat
7
 * @package     Apparat\Object
8
 * @subpackage  Apparat\Object\Domain
9
 * @author      Joschi Kuphal <[email protected]> / @jkphl
10
 * @copyright   Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
11
 * @license     http://opensource.org/licenses/MIT The MIT License (MIT)
12
 */
13
14
/***********************************************************************************
15
 *  The MIT License (MIT)
16
 *
17
 *  Copyright © 2016 Joschi Kuphal <[email protected]> / @jkphl
18
 *
19
 *  Permission is hereby granted, free of charge, to any person obtaining a copy of
20
 *  this software and associated documentation files (the "Software"), to deal in
21
 *  the Software without restriction, including without limitation the rights to
22
 *  use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
23
 *  the Software, and to permit persons to whom the Software is furnished to do so,
24
 *  subject to the following conditions:
25
 *
26
 *  The above copyright notice and this permission notice shall be included in all
27
 *  copies or substantial portions of the Software.
28
 *
29
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
30
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
31
 *  FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
32
 *  COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
33
 *  IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
34
 *  CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
35
 ***********************************************************************************/
36
37
namespace Apparat\Object\Domain\Model\Object;
38
39
use Apparat\Kernel\Ports\Kernel;
40
use Apparat\Object\Domain\Model\Object\Traits\DomainPropertiesTrait;
41
use Apparat\Object\Domain\Model\Object\Traits\MetaPropertiesTrait;
42
use Apparat\Object\Domain\Model\Object\Traits\PayloadTrait;
43
use Apparat\Object\Domain\Model\Object\Traits\ProcessingInstructionsTrait;
44
use Apparat\Object\Domain\Model\Object\Traits\RelationsTrait;
45
use Apparat\Object\Domain\Model\Object\Traits\SystemPropertiesTrait;
46
use Apparat\Object\Domain\Model\Path\RepositoryPath;
47
use Apparat\Object\Domain\Model\Path\RepositoryPathInterface;
48
use Apparat\Object\Domain\Model\Properties\AbstractDomainProperties;
49
use Apparat\Object\Domain\Model\Properties\InvalidArgumentException as PropertyInvalidArgumentException;
50
use Apparat\Object\Domain\Model\Properties\MetaProperties;
51
use Apparat\Object\Domain\Model\Properties\ProcessingInstructions;
52
use Apparat\Object\Domain\Model\Properties\Relations;
53
use Apparat\Object\Domain\Model\Properties\SystemProperties;
54
use Apparat\Object\Domain\Repository\Service;
55
56
/**
57
 * Abstract object
58
 *
59
 * @package Apparat\Object
60
 * @subpackage Apparat\Object\Domain
61
 */
62
abstract class AbstractObject implements ObjectInterface
63
{
64
    /**
65
     * Use traits
66
     */
67
    use SystemPropertiesTrait, MetaPropertiesTrait, DomainPropertiesTrait, RelationsTrait,
68
        ProcessingInstructionsTrait, PayloadTrait;
69
    /**
70
     * Clean state
71
     *
72
     * @var int
73
     */
74
    const STATE_CLEAN = 0;
75
    /**
76
     * Dirty state
77
     *
78
     * @var int
79
     */
80
    const STATE_DIRTY = 1;
81
    /**
82
     * Mutated state
83
     *
84
     * @var int
85
     */
86
    const STATE_MUTATED = 2;
87
    /**
88
     * Published state
89
     *
90
     * @var int
91
     */
92
    const STATE_PUBLISHED = 4;
93
    /**
94
     * Deleted state
95
     *
96
     * @var int
97
     */
98
    const STATE_DELETED = 8;
99
    /**
100
     * Undeleted state
101
     *
102
     * @var int
103
     */
104
    const STATE_UNDELETED = 16;
105
    /**
106
     * Repository path
107
     *
108
     * @var RepositoryPathInterface
109
     */
110
    protected $path;
111
    /**
112
     * Latest revision index
113
     *
114
     * @var Revision
115
     */
116
    protected $latestRevision;
117
    /**
118
     * Object state
119
     *
120
     * @var int
121
     */
122
    protected $state = self::STATE_CLEAN;
123
    /**
124
     * Property collection states
125
     *
126
     * @var array
127
     */
128
    protected $collectionStates = [];
129
130
    /**
131
     * Object constructor
132
     *
133
     * @param RepositoryPathInterface $path Object repository path
134
     * @param string $payload Object payload
135
     * @param array $propertyData Property data
136
     */
137 26
    public function __construct(RepositoryPathInterface $path, $payload = '', array $propertyData = [])
138
    {
139
        // If the domain property collection class is invalid
140 26
        if (!$this->domainPropertyCClass
141 26
            || !class_exists($this->domainPropertyCClass)
142 26
            || !(new \ReflectionClass($this->domainPropertyCClass))->isSubclassOf(AbstractDomainProperties::class)
143 26
        ) {
144 1
            throw new PropertyInvalidArgumentException(
145 1
                sprintf(
146 1
                    'Invalid domain property collection class "%s"',
147 1
                    $this->domainPropertyCClass
148 1
                ),
149
                PropertyInvalidArgumentException::INVALID_DOMAIN_PROPERTY_COLLECTION_CLASS
150 1
            );
151
        }
152
153
        // Right after instantiation it's always the current revision
154 25
        $this->path = $path->setRevision(Revision::current());
155
156
        // Load the current revision data
157 25
        $this->loadRevisionData($payload, $propertyData);
158
159
        // Save the latest revision index
160 24
        $this->latestRevision = $this->getRevision();
161 24
    }
162
163
    /**
164
     * Load object revision data
165
     *
166
     * @param string $payload Object payload
167
     * @param array $propertyData Property data
168
     */
169 25
    protected function loadRevisionData($payload = '', array $propertyData = [])
170
    {
171 25
        $this->payload = $payload;
172
173
        // Instantiate the system properties
174 25
        $systemPropertyData = (empty($propertyData[SystemProperties::COLLECTION]) ||
175 25
            !is_array(
176 25
                $propertyData[SystemProperties::COLLECTION]
177 25
            )) ? [] : $propertyData[SystemProperties::COLLECTION];
178 25
        $this->systemProperties = Kernel::create(SystemProperties::class, [$systemPropertyData, $this]);
179
180
        // Instantiate the meta properties
181 24
        $metaPropertyData = (empty($propertyData[MetaProperties::COLLECTION]) ||
182 23
            !is_array(
183 23
                $propertyData[MetaProperties::COLLECTION]
184 24
            )) ? [] : $propertyData[MetaProperties::COLLECTION];
185
        /** @var MetaProperties $metaProperties */
186 24
        $metaProperties = Kernel::create(MetaProperties::class, [$metaPropertyData, $this]);
187 24
        $this->setMetaProperties($metaProperties, true);
188
189
        // Instantiate the domain properties
190 24
        $domainPropertyData = (empty($propertyData[AbstractDomainProperties::COLLECTION]) ||
191 23
            !is_array(
192 23
                $propertyData[AbstractDomainProperties::COLLECTION]
193 24
            )) ? [] : $propertyData[AbstractDomainProperties::COLLECTION];
194
        /** @var AbstractDomainProperties $domainProperties */
195 24
        $domainProperties = Kernel::create($this->domainPropertyCClass, [$domainPropertyData, $this]);
196 24
        $this->setDomainProperties($domainProperties, true);
197
198
        // Instantiate the processing instructions
199 24
        $procInstData = (empty($propertyData[ProcessingInstructions::COLLECTION]) ||
200 20
            !is_array(
201 20
                $propertyData[ProcessingInstructions::COLLECTION]
202 24
            )) ? [] : $propertyData[ProcessingInstructions::COLLECTION];
203
        /** @var ProcessingInstructions $procInstCollection */
204 24
        $procInstCollection = Kernel::create(ProcessingInstructions::class, [$procInstData, $this]);
205 24
        $this->setProcessingInstructions($procInstCollection, true);
206
207
        // Instantiate the object relations
208 24
        $relationData = (empty($propertyData[Relations::COLLECTION]) ||
209 23
            !is_array(
210 23
                $propertyData[Relations::COLLECTION]
211 24
            )) ? [] : $propertyData[Relations::COLLECTION];
212
        /** @var Relations $relationCollection */
213 24
        $relationCollection = Kernel::create(Relations::class, [$relationData, $this]);
214 24
        $this->setRelations($relationCollection, true);
215
216
        // Reset the object state to clean
217 24
        $this->state = self::STATE_CLEAN;
218 24
    }
219
220
    /**
221
     * Return whether the object is in mutated state
222
     *
223
     * @return boolean Mutated state
224
     */
225 3
    public function isMutated()
226
    {
227 3
        return !!($this->state & self::STATE_MUTATED);
228
    }
229
230
    /**
231
     * Use a specific object revision
232
     *
233
     * @param Revision $revision Revision to be used
234
     * @return ObjectInterface Object
235
     * @throws OutOfBoundsException If the requested revision is invalid
236
     */
237 23
    public function useRevision(Revision $revision)
238
    {
239 23
        $isCurrentRevision = false;
240
241
        // If the requested revision is invalid
242 23
        if (!$revision->isCurrent() &&
243
            (($revision->getRevision() < 1) || ($revision->getRevision() > $this->latestRevision->getRevision()))
244 23
        ) {
245
            throw new OutOfBoundsException(
246
                sprintf('Invalid object revision "%s"', $revision->getRevision()),
247
                OutOfBoundsException::INVALID_OBJECT_REVISION
248
            );
249
        }
250
251
        // If the current revision got requested
252 23
        if ($revision->isCurrent()) {
253 23
            $isCurrentRevision = true;
254 23
            $revision = $this->latestRevision;
255 23
        }
256
257
        // If the requested revision is not already used
258 23
        if ($revision != $this->getRevision()) {
259
            /** @var ManagerInterface $objectManager */
260
            $objectManager = Kernel::create(Service::class)->getObjectManager();
261
262
            // Load the requested object revision resource
263
            /** @var Revision $newRevision */
264
            $newRevision = $isCurrentRevision ? Revision::current() : $revision;
265
            /** @var RepositoryPath $newRevisionPath */
266
            $newRevisionPath = $this->path->setRevision($newRevision);
267
            $revisionResource = $objectManager->loadObject($newRevisionPath);
268
269
            // Load the revision resource data
270
            $this->loadRevisionData($revisionResource->getPayload(), $revisionResource->getPropertyData());
271
272
            // Set the current revision path
273
            $this->path = $newRevisionPath;
274
        }
275
276 23
        return $this;
277
    }
278
279
    /**
280
     * Return the object repository path
281
     *
282
     * @return RepositoryPathInterface Object repository path
283
     */
284 24
    public function getRepositoryPath()
285
    {
286 24
        return $this->path;
287
    }
288
289
    /**
290
     * Return the object property data
291
     *
292
     * @return array Object property data
293
     */
294 5
    public function getPropertyData()
295
    {
296 5
        $propertyData = array_filter([
297 5
            SystemProperties::COLLECTION => $this->systemProperties->toArray(),
298 5
            MetaProperties::COLLECTION => $this->metaProperties->toArray(),
299 5
            AbstractDomainProperties::COLLECTION => $this->domainProperties->toArray(),
300 5
            ProcessingInstructions::COLLECTION => $this->processingInstructions->toArray(),
301 5
            Relations::COLLECTION => $this->relations->toArray(),
302 5
        ], function (array $collection) {
303 5
            return (boolean)count($collection);
304 5
        });
305
306 5
        return $propertyData;
307
    }
308
309
    /**
310
     * Return the absolute object URL
311
     *
312
     * @return string
313
     */
314 4
    public function getAbsoluteUrl()
315
    {
316 4
        return getenv('APPARAT_BASE_URL').ltrim($this->path->getRepository()->getUrl(), '/').strval($this->path);
317
    }
318
319
    /**
320
     * Persist the current object revision
321
     *
322
     * @return ObjectInterface Object
323
     */
324 1
    public function persist()
325
    {
326
        // If this is not the latest revision
327 1
        if ($this->getRevision() != $this->latestRevision) {
328
            throw new RuntimeException(
329
                sprintf(
330
                    'Cannot persist revision %s/%s',
331
                    $this->getRevision()->getRevision(),
332
                    $this->latestRevision->getRevision()
333
                ),
334
                RuntimeException::CANNOT_PERSIST_EARLIER_REVISION
335
            );
336
        }
337
338
        // Update the object repository
339 1
        $this->path->getRepository()->updateObject($this);
340
341
        // Reset to a clean state
342 1
        $this->state &= self::STATE_CLEAN;
343
344 1
        return $this;
345
    }
346
347
    /**
348
     * Publish the current object revision
349
     *
350
     * @return ObjectInterface Object
351
     */
352 1
    public function publish()
353
    {
354
        // If this is an unpublished draft
355 1
        if ($this->isDraft() & !($this->state & self::STATE_PUBLISHED)) {
356
            // TODO: Send signal
357
358
            // Update system properties
359 1
            $this->setSystemProperties($this->systemProperties->publish(), true);
360
361
            // Remove the draft flag from the repository path
362 1
            $this->path = $this->path->setDraft(false);
363
364
            // Enable the dirty & published state
365 1
            $this->state |= (self::STATE_DIRTY | self::STATE_PUBLISHED);
366 1
        }
367
368 1
        return $this;
369
    }
370
371
    /**
372
     * Return the object draft mode
373
     *
374
     * @return boolean Object draft mode
375
     */
376 5
    public function isDraft()
377
    {
378 5
        return $this->systemProperties->isDraft() || $this->hasBeenPublished();
379
    }
380
381
    /**
382
     * Return whether the object is in published state
383
     *
384
     * @return boolean Published state
385
     */
386
    public function isPublished()
387
    {
388
        return $this->systemProperties->isPublished();
389
    }
390
391
    /**
392
     * Return whether the object has just been published
393
     *
394
     * @return boolean Object has just been published
395
     */
396 5
    public function hasBeenPublished()
397
    {
398 5
        return !!($this->state & self::STATE_PUBLISHED);
399
    }
400
401
    /**
402
     * Delete the object and all its revisions
403
     *
404
     * @return ObjectInterface Object
405
     */
406
    public function delete()
407
    {
408
        // If this object is not already deleted
409
        if (!$this->isDeleted() && !$this->hasBeenDeleted()) {
410
            // TODO: Send delete signal
411
412
            // Update system properties
413
            $this->setSystemProperties($this->systemProperties->delete(), true);
414
415
            // TODO: Modify the object path so that it's deleted
416
417
            // Flag the object as just deleted
418
            $this->state |= self::STATE_DIRTY;
419
            $this->state |= self::STATE_DELETED;
420
            $this->state &= ~self::STATE_UNDELETED;
421
        }
422
423
        return $this;
424
425
    }
426
427
    /**
428
     * Return whether the object has been deleted
429
     *
430
     * @return boolean Object is deleted
431
     */
432
    public function isDeleted()
433
    {
434
        return $this->systemProperties->isDeleted();
435
    }
436
437
    /**
438
     * Return whether the object has just been deleted
439
     *
440
     * @return boolean Object has just been deleted
441
     */
442 1
    public function hasBeenDeleted()
443
    {
444 1
        return !!($this->state & self::STATE_DELETED);
445
    }
446
447
    /**
448
     * Undelete the object and all its revisions
449
     *
450
     * @return ObjectInterface Object
451
     */
452
    public function undelete()
453
    {
454
        // If this object is already deleted
455
        if ($this->isDeleted() && !$this->hasBeenUndeleted()) {
456
            // TODO: Send undelete signal
457
458
            // Update system properties
459
            $this->setSystemProperties($this->systemProperties->undelete(), true);
460
461
            // TODO: Modify the object path so that it's not deleted
462
463
            // Flag the object as just undeleted
464
            $this->state |= self::STATE_DIRTY;
465
            $this->state |= self::STATE_UNDELETED;
466
            $this->state &= ~self::STATE_DELETED;
467
        }
468
    }
469
470
    /**
471
     * Return whether the object has just been undeleted
472
     *
473
     * @return boolean Object has just been undeleted
474
     */
475 1
    public function hasBeenUndeleted()
476
    {
477 1
        return !!($this->state & self::STATE_UNDELETED);
478
    }
479
480
    /**
481
     * Return whether the object is in dirty state
482
     *
483
     * @return boolean Dirty state
484
     */
485 3
    public function isDirty()
486
    {
487 3
        return !!($this->state & self::STATE_DIRTY);
488
    }
489
490
    /**
491
     * Set the object state to mutated
492
     */
493 4
    protected function setMutatedState()
494
    {
495
        // If this object is not in mutated state yet
496 4
        if (!($this->state & self::STATE_MUTATED) && !$this->isDraft()) {
497
            // TODO: Send signal
498 4
            $this->convertToDraft();
499 4
        }
500
501
        // Enable the mutated state
502 4
        $this->state |= self::STATE_MUTATED;
503
504
        // Enable the dirty state
505 4
        $this->setDirtyState();
506 4
    }
507
508
    /**
509
     * Convert this object revision into a draft
510
     */
511 4
    protected function convertToDraft()
512
    {
513
        // Increment the latest revision number
514 4
        $this->latestRevision = $this->latestRevision->increment();
515
516
        // Create draft system properties
517 4
        $this->systemProperties = $this->systemProperties->createDraft($this->latestRevision);
518
519
        // Adapt the system properties collection state
520 4
        $this->collectionStates[SystemProperties::COLLECTION] = spl_object_hash($this->systemProperties);
521
522
        // Set the draft flag on the repository path
523 4
        $this->path = $this->path->setDraft(true)->setRevision(Revision::current());
524
525
        // If this is not already a draft ...
526
        // Recreate the system properties
527
        // Copy the object ID
528
        // Copy the object type
529
        // Set the revision number to latest revision + 1
530
        // Set the creation date to now
531
        // Set no publication date
532
        // Set the draft flag on the repository path
533
        // Increase the latest revision by 1
534
535
        // Else if this is a draft
536
        // No action needed
537 4
    }
538
539
    /**
540
     * Set the object state to dirty
541
     */
542 7
    protected function setDirtyState()
543
    {
544
        // If this object is not in dirty state yet
545 7
        if (!($this->state & self::STATE_DIRTY)) {
546
            // TODO: Send signal
547 7
        }
548
549
        // Enable the dirty state
550 7
        $this->state |= self::STATE_DIRTY;
551
552
        // Update the modification timestamp
553 7
        $this->setSystemProperties($this->systemProperties->touch(), true);
554 7
    }
555
}
556