Completed
Push — master ( f0adbf...7c6605 )
by Joschi
03:16 queued 30s
created

AbstractObject::setMutatedState()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 2

Importance

Changes 12
Bugs 0 Features 6
Metric Value
cc 2
eloc 5
c 12
b 0
f 6
nc 2
nop 0
dl 0
loc 14
ccs 7
cts 7
cp 1
crap 2
rs 9.4285
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\IterableTrait;
42
use Apparat\Object\Domain\Model\Object\Traits\MetaPropertiesTrait;
43
use Apparat\Object\Domain\Model\Object\Traits\PayloadTrait;
44
use Apparat\Object\Domain\Model\Object\Traits\ProcessingInstructionsTrait;
45
use Apparat\Object\Domain\Model\Object\Traits\RelationsTrait;
46
use Apparat\Object\Domain\Model\Object\Traits\SystemPropertiesTrait;
47
use Apparat\Object\Domain\Model\Path\RepositoryPath;
48
use Apparat\Object\Domain\Model\Path\RepositoryPathInterface;
49
use Apparat\Object\Domain\Model\Properties\AbstractDomainProperties;
50
use Apparat\Object\Domain\Model\Properties\InvalidArgumentException as PropertyInvalidArgumentException;
51
use Apparat\Object\Domain\Model\Properties\MetaProperties;
52
use Apparat\Object\Domain\Model\Properties\ProcessingInstructions;
53
use Apparat\Object\Domain\Model\Properties\Relations;
54
use Apparat\Object\Domain\Model\Properties\SystemProperties;
55
use Apparat\Object\Domain\Repository\Service;
56
57
/**
58
 * Abstract object
59
 *
60
 * @package Apparat\Object
61
 * @subpackage Apparat\Object\Domain
62
 */
63
abstract class AbstractObject implements ObjectInterface, \Iterator, \Countable
64
{
65
    /**
66
     * Use traits
67
     */
68
    use SystemPropertiesTrait, MetaPropertiesTrait, DomainPropertiesTrait, RelationsTrait,
69
        ProcessingInstructionsTrait, PayloadTrait, IterableTrait;
70
    /**
71
     * Clean state
72
     *
73
     * @var int
74
     */
75
    const STATE_CLEAN = 0;
76
    /**
77
     * Modified state
78
     *
79
     * @var int
80
     */
81
    const STATE_MODIFIED = 1;
82
    /**
83
     * Mutated state
84
     *
85
     * @var int
86
     */
87
    const STATE_MUTATED = 2;
88
    /**
89
     * Published state
90
     *
91
     * @var int
92
     */
93
    const STATE_PUBLISHED = 4;
94
    /**
95
     * Deleted state
96
     *
97
     * @var int
98
     */
99
    const STATE_DELETED = 8;
100
    /**
101
     * Undeleted state
102
     *
103
     * @var int
104
     */
105
    const STATE_UNDELETED = 16;
106
    /**
107
     * Repository path
108
     *
109
     * @var RepositoryPathInterface
110
     */
111
    protected $path;
112
    /**
113
     * Latest revision
114
     *
115
     * @var Revision
116
     */
117
    protected $latestRevision;
118
    /**
119
     * Object state
120
     *
121
     * @var int
122
     */
123
    protected $state = self::STATE_CLEAN;
124
    /**
125
     * Property collection states
126
     *
127
     * @var array
128
     */
129
    protected $collectionStates = [];
130
131
    /**
132
     * Object constructor
133
     *
134
     * @param RepositoryPathInterface $path Object repository path
135
     * @param string $payload Object payload
136
     * @param array $propertyData Property data
137
     */
138 26
    public function __construct(RepositoryPathInterface $path, $payload = '', array $propertyData = [])
139
    {
140
        // If the domain property collection class is invalid
141 26
        if (!$this->domainPropertyCClass
142 26
            || !class_exists($this->domainPropertyCClass)
143 26
            || !(new \ReflectionClass($this->domainPropertyCClass))->isSubclassOf(AbstractDomainProperties::class)
144 26
        ) {
145 1
            throw new PropertyInvalidArgumentException(
146 1
                sprintf(
147 1
                    'Invalid domain property collection class "%s"',
148 1
                    $this->domainPropertyCClass
149 1
                ),
150
                PropertyInvalidArgumentException::INVALID_DOMAIN_PROPERTY_COLLECTION_CLASS
151 1
            );
152
        }
153
154
        // Right after instantiation it's always the current revision
155 25
        $this->path = $path->setRevision(Revision::current($path->getRevision()->isDraft()));
156
157
        // Load the current revision data
158 25
        $this->loadRevisionData($payload, $propertyData);
159
160
        // Determine the latest revision number (considering a possible draft)
161 24
        $this->latestRevision = $this->hasDraft()
162 24
            ? Kernel::create(Revision::class, [$this->getRevision()->getRevision() + 1, true])
163 24
            : $this->getRevision();
164 24
    }
165
166
    /**
167
     * Load object revision data
168
     *
169
     * @param string $payload Object payload
170
     * @param array $propertyData Property data
171
     */
172 25
    protected function loadRevisionData($payload = '', array $propertyData = [])
173
    {
174 25
        $this->payload = $payload;
175
176
        // Instantiate the system properties
177 25
        $systemPropertyData = (empty($propertyData[SystemProperties::COLLECTION]) ||
178 25
            !is_array(
179 25
                $propertyData[SystemProperties::COLLECTION]
180 25
            )) ? [] : $propertyData[SystemProperties::COLLECTION];
181 25
        $this->systemProperties = Kernel::create(SystemProperties::class, [$systemPropertyData, $this]);
182
183
        // Instantiate the meta properties
184 24
        $metaPropertyData = (empty($propertyData[MetaProperties::COLLECTION]) ||
185 23
            !is_array(
186 23
                $propertyData[MetaProperties::COLLECTION]
187 24
            )) ? [] : $propertyData[MetaProperties::COLLECTION];
188
        /** @var MetaProperties $metaProperties */
189 24
        $metaProperties = Kernel::create(MetaProperties::class, [$metaPropertyData, $this]);
190 24
        $this->setMetaProperties($metaProperties, true);
191
192
        // Instantiate the domain properties
193 24
        $domainPropertyData = (empty($propertyData[AbstractDomainProperties::COLLECTION]) ||
194 23
            !is_array(
195 23
                $propertyData[AbstractDomainProperties::COLLECTION]
196 24
            )) ? [] : $propertyData[AbstractDomainProperties::COLLECTION];
197
        /** @var AbstractDomainProperties $domainProperties */
198 24
        $domainProperties = Kernel::create($this->domainPropertyCClass, [$domainPropertyData, $this]);
199 24
        $this->setDomainProperties($domainProperties, true);
200
201
        // Instantiate the processing instructions
202 24
        $procInstData = (empty($propertyData[ProcessingInstructions::COLLECTION]) ||
203 20
            !is_array(
204 20
                $propertyData[ProcessingInstructions::COLLECTION]
205 24
            )) ? [] : $propertyData[ProcessingInstructions::COLLECTION];
206
        /** @var ProcessingInstructions $procInstCollection */
207 24
        $procInstCollection = Kernel::create(ProcessingInstructions::class, [$procInstData, $this]);
208 24
        $this->setProcessingInstructions($procInstCollection, true);
209
210
        // Instantiate the object relations
211 24
        $relationData = (empty($propertyData[Relations::COLLECTION]) ||
212 23
            !is_array(
213 23
                $propertyData[Relations::COLLECTION]
214 24
            )) ? [] : $propertyData[Relations::COLLECTION];
215
        /** @var Relations $relationCollection */
216 24
        $relationCollection = Kernel::create(Relations::class, [$relationData, $this]);
217 24
        $this->setRelations($relationCollection, true);
218
219
        // Reset the object state to clean
220 24
        $this->state = self::STATE_CLEAN;
221 24
    }
222
223
    /**
224
     * Return whether the object is in mutated state
225
     *
226
     * @return boolean Mutated state
227
     */
228 3
    public function hasBeenMutated()
229
    {
230 3
        return !!($this->state & self::STATE_MUTATED);
231
    }
232
233
    /**
234
     * Use a specific object revision
235
     *
236
     * @param Revision $revision Revision to be used
237
     * @return ObjectInterface Object
238
     * @throws OutOfBoundsException If the requested revision is invalid
239
     */
240 23
    public function useRevision(Revision $revision)
241
    {
242 23
        $isCurrentRevision = false;
243
244
        // If the requested revision is invalid
245 23
        if (!$revision->isCurrent() &&
246
            (($revision->getRevision() < 1) || ($revision->getRevision() > $this->latestRevision->getRevision()))
247 23
        ) {
248
            throw new OutOfBoundsException(
249
                sprintf('Invalid object revision "%s"', $revision->getRevision()),
250
                OutOfBoundsException::INVALID_OBJECT_REVISION
251
            );
252
        }
253
254
        // If the current revision got requested
255 23
        if ($revision->isCurrent()) {
256 23
            $isCurrentRevision = true;
257 23
            $revision = $this->latestRevision;
258 23
        }
259
260
        // If the requested revision is not already used
261 23
        if ($revision != $this->getRevision()) {
262
263
            /** @var ManagerInterface $objectManager */
264
            $objectManager = Kernel::create(Service::class)->getObjectManager();
265
266
            // Load the requested object revision resource
267
            /** @var Revision $newRevision */
268
            $newRevision = $isCurrentRevision ? Revision::current() : $revision;
269
            /** @var RepositoryPath $newRevisionPath */
270
            $newRevisionPath = $this->path->setRevision($newRevision);
271
//print_r($newRevisionPath);
272
//            echo get_class($this->path).$this->path.' <-> '.$newRevisionPath.PHP_EOL;
273
274
            $revisionResource = $objectManager->loadObjectResource($newRevisionPath);
275
276
//            echo 'RESOURCE DATA '.$newRevisionPath.PHP_EOL;
277
//            print_r($revisionResource->getPropertyData());
278
279
            // Load the revision resource data
280
            $this->loadRevisionData($revisionResource->getPayload(), $revisionResource->getPropertyData());
281
282
            // Set the current revision path
283
            $this->path = $newRevisionPath;
284
        }
285
286 23
        return $this;
287
    }
288
289
    /**
290
     * Return the object repository path
291
     *
292
     * @return RepositoryPathInterface Object repository path
293
     */
294 24
    public function getRepositoryPath()
295
    {
296 24
        return $this->path;
297
    }
298
299
    /**
300
     * Return the object property data
301
     *
302
     * @return array Object property data
303
     */
304 5
    public function getPropertyData()
305
    {
306 5
        $propertyData = array_filter([
307 5
            SystemProperties::COLLECTION => $this->systemProperties->toArray(),
308 5
            MetaProperties::COLLECTION => $this->metaProperties->toArray(),
309 5
            AbstractDomainProperties::COLLECTION => $this->domainProperties->toArray(),
310 5
            ProcessingInstructions::COLLECTION => $this->processingInstructions->toArray(),
311 5
            Relations::COLLECTION => $this->relations->toArray(),
312 5
        ], function (array $collection) {
313 5
            return (boolean)count($collection);
314 5
        });
315
316 5
        return $propertyData;
317
    }
318
319
    /**
320
     * Return the absolute object URL
321
     *
322
     * @return string
323
     */
324 4
    public function getAbsoluteUrl()
325
    {
326 4
        return getenv('APPARAT_BASE_URL').ltrim($this->path->getRepository()->getUrl(), '/').strval($this->path);
327
    }
328
329
    /**
330
     * Persist the current object revision
331
     *
332
     * @return ObjectInterface Object
333
     */
334 1
    public function persist()
335
    {
336
        // If this is not the latest revision
337 1
        if ($this->getRevision()->getRevision() != $this->latestRevision->getRevision()) {
338
            throw new RuntimeException(
339
                sprintf(
340
                    'Cannot persist revision %s/%s',
341
                    $this->getRevision()->getRevision(),
342
                    $this->latestRevision->getRevision()
343
                ),
344
                RuntimeException::CANNOT_PERSIST_EARLIER_REVISION
345
            );
346
        }
347
348
//        echo $this->path.PHP_EOL;
349
350
        // Update the object repository
351 1
        $this->path->getRepository()->updateObject($this);
352
353
        // Reset to a clean state
354 1
        $this->state &= self::STATE_CLEAN;
355 1
        $this->latestRevision = $this->getRevision();
356 1
        $this->path = $this->path->setRevision(Revision::current($this->latestRevision->isDraft()));
357
358
//        echo 'persisted: '.PHP_EOL;
359
//        print_r($this->getRevision());
360
//        echo $this->path;
361
//        echo PHP_EOL.'---------------'.PHP_EOL;
362
363 1
        return $this;
364
    }
365
366
    /**
367
     * Publish the current object revision
368
     *
369
     * @return ObjectInterface Object
370
     */
371 1
    public function publish()
372
    {
373
        // If this is an unpublished draft
374 1
        if ($this->isDraft() & !($this->state & self::STATE_PUBLISHED)) {
375
            // TODO: Send signal
376
377
            // Update system properties
378 1
            $this->setSystemProperties($this->systemProperties->publish(), true);
379
380
            // Remove the draft flag from the repository path
381 1
            $this->path = $this->path->setRevision(Revision::current());
382
383
            // Enable the modified & published state
384 1
            $this->state |= (self::STATE_MODIFIED | self::STATE_PUBLISHED);
385 1
        }
386
387 1
        return $this;
388
    }
389
390
    /**
391
     * Return the object draft mode
392
     *
393
     * @return boolean Object draft mode
394
     */
395 5
    public function isDraft()
396
    {
397 5
        return $this->systemProperties->isDraft() || $this->hasBeenPublished();
398
    }
399
400
    /**
401
     * Return whether the object is in published state
402
     *
403
     * @return boolean Published state
404
     */
405
    public function isPublished()
406
    {
407
        return $this->systemProperties->isPublished();
408
    }
409
410
    /**
411
     * Return whether the object has just been published
412
     *
413
     * @return boolean Object has just been published
414
     */
415 5
    public function hasBeenPublished()
416
    {
417 5
        return !!($this->state & self::STATE_PUBLISHED);
418
    }
419
420
    /**
421
     * Delete the object and all its revisions
422
     *
423
     * @return ObjectInterface Object
424
     */
425
    public function delete()
426
    {
427
        // If this object is not already deleted
428
        if (!$this->isDeleted() && !$this->hasBeenDeleted()) {
429
            // TODO: Send delete signal
430
431
            // Update system properties
432
            $this->setSystemProperties($this->systemProperties->delete(), true);
433
434
            // TODO: Modify the object path so that it's deleted
435
436
            // Flag the object as just deleted
437
            $this->state |= self::STATE_MODIFIED;
438
            $this->state |= self::STATE_DELETED;
439
            $this->state &= ~self::STATE_UNDELETED;
440
        }
441
442
        return $this;
443
444
    }
445
446
    /**
447
     * Return whether the object has been deleted
448
     *
449
     * @return boolean Object is deleted
450
     */
451
    public function isDeleted()
452
    {
453
        return $this->systemProperties->isDeleted();
454
    }
455
456
    /**
457
     * Return whether the object has just been deleted
458
     *
459
     * @return boolean Object has just been deleted
460
     */
461 1
    public function hasBeenDeleted()
462
    {
463 1
        return !!($this->state & self::STATE_DELETED);
464
    }
465
466
    /**
467
     * Undelete the object and all its revisions
468
     *
469
     * @return ObjectInterface Object
470
     */
471
    public function undelete()
472
    {
473
        // If this object is already deleted
474
        if ($this->isDeleted() && !$this->hasBeenUndeleted()) {
475
            // TODO: Send undelete signal
476
477
            // Update system properties
478
            $this->setSystemProperties($this->systemProperties->undelete(), true);
479
480
            // TODO: Modify the object path so that it's not deleted
481
482
            // Flag the object as just undeleted
483
            $this->state |= self::STATE_MODIFIED;
484
            $this->state |= self::STATE_UNDELETED;
485
            $this->state &= ~self::STATE_DELETED;
486
        }
487
    }
488
489
    /**
490
     * Return whether the object has just been undeleted
491
     *
492
     * @return boolean Object has just been undeleted
493
     */
494 1
    public function hasBeenUndeleted()
495
    {
496 1
        return !!($this->state & self::STATE_UNDELETED);
497
    }
498
499
    /**
500
     * Return whether the object is in modified state
501
     *
502
     * @return boolean Modified state
503
     */
504 3
    public function hasBeenModified()
505
    {
506 3
        return !!($this->state & self::STATE_MODIFIED);
507
    }
508
509
    /**
510
     * Set the object state to mutated
511
     */
512 4
    protected function setMutatedState()
513
    {
514
        // Make this object a draft if not already the case
515 4
        if (!$this->isDraft()) {
516
            // TODO: Send signal
517 4
            $this->convertToDraft();
518 4
        }
519
520
        // Enable the mutated state
521 4
        $this->state |= self::STATE_MUTATED;
522
523
        // Enable the modified state
524 4
        $this->setModifiedState();
525 4
    }
526
527
    /**
528
     * Return whether this object already has a draft revision
529
     */
530 24
    protected function hasDraft()
531
    {
532
        /** @var ManagerInterface $objectManager */
533 24
        $objectManager = Kernel::create(Service::class)->getObjectManager();
534 24
        $draftPath = $this->path->setRevision(Revision::current(true));
535 24
        return $objectManager->objectResourceExists($draftPath);
536
    }
537
538
    /**
539
     * Convert this object revision into a draft
540
     */
541 4
    protected function convertToDraft()
542
    {
543
        // Set the current revision to the latest revision
544 4
        $draftRevision = $this->latestRevision;
545
        
546
        // If that's not a draft revision: Increment and enable draft mode
547 4
        if (!$draftRevision->isDraft()) {
548 4
            $draftRevision = $this->latestRevision = $draftRevision->increment()->setDraft(true);
549 4
        }
550
551
        // Set the system properties to draft mode
552 4
        $this->setSystemProperties($this->systemProperties->createDraft($draftRevision), true);
553
554
        // Set the draft flag on the repository path
555 4
        $this->path = $this->path->setRevision($draftRevision);
556 4
    }
557
558
    /**
559
     * Set the object state to modified
560
     */
561 7
    protected function setModifiedState()
562
    {
563
        // If this object is not in modified state yet
564 7
        if (!($this->state & self::STATE_MODIFIED)) {
565
            // TODO: Send signal
566 7
        }
567
568
        // Enable the modified state
569 7
        $this->state |= self::STATE_MODIFIED;
570
571
        // Update the modification timestamp
572 7
        $this->setSystemProperties($this->systemProperties->touch(), true);
573 7
    }
574
}
575