Completed
Push — master ( 8c9387...9f1772 )
by Joschi
03:25
created

AbstractObject   C

Complexity

Total Complexity 47

Size/Duplication

Total Lines 458
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 88.72%

Importance

Changes 36
Bugs 1 Features 12
Metric Value
c 36
b 1
f 12
dl 0
loc 458
ccs 118
cts 133
cp 0.8872
rs 5.0679
wmc 47
lcom 1
cbo 19

20 Methods

Rating   Name   Duplication   Size   Complexity  
B __construct() 0 25 4
F loadRevisionData() 0 50 11
A isMutated() 0 4 1
C useRevision() 0 39 7
A getRepositoryPath() 0 4 1
A getPropertyData() 0 14 1
A getPayload() 0 4 1
A setPayload() 0 10 2
A setMutatedState() 0 11 3
A isDraft() 0 4 2
A isPublished() 0 4 1
B convertToDraft() 0 27 1
A getAbsoluteUrl() 0 4 1
A getProcessingInstruction() 0 4 1
A setProcessingInstruction() 0 5 1
A persist() 0 22 2
A publish() 0 21 2
A setPublishedState() 0 10 2
A isDirty() 0 4 1
A setDirtyState() 0 10 2

How to fix   Complexity   

Complex Class

Complex classes like AbstractObject often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use AbstractObject, and based on these observations, apply Extract Interface, too.

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\ProcessingInstructionsTrait;
43
use Apparat\Object\Domain\Model\Object\Traits\RelationsTrait;
44
use Apparat\Object\Domain\Model\Object\Traits\SystemPropertiesTrait;
45
use Apparat\Object\Domain\Model\Path\RepositoryPath;
46
use Apparat\Object\Domain\Model\Path\RepositoryPathInterface;
47
use Apparat\Object\Domain\Model\Properties\AbstractDomainProperties;
48
use Apparat\Object\Domain\Model\Properties\InvalidArgumentException as PropertyInvalidArgumentException;
49
use Apparat\Object\Domain\Model\Properties\MetaProperties;
50
use Apparat\Object\Domain\Model\Properties\ProcessingInstructions;
51
use Apparat\Object\Domain\Model\Properties\Relations;
52
use Apparat\Object\Domain\Model\Properties\SystemProperties;
53
use Apparat\Object\Domain\Repository\Service;
54
55
/**
56
 * Abstract object
57
 *
58
 * @package Apparat\Object
59
 * @subpackage Apparat\Object\Domain
60
 */
61
abstract class AbstractObject implements ObjectInterface
62
{
63
    /**
64
     * Use traits
65
     */
66
    use SystemPropertiesTrait, MetaPropertiesTrait, DomainPropertiesTrait, RelationsTrait, ProcessingInstructionsTrait;
67
    /**
68
     * Clean state
69
     *
70
     * @var int
71
     */
72
    const STATE_CLEAN = 0;
73
    /**
74
     * Dirty state
75
     *
76
     * @var int
77
     */
78
    const STATE_DIRTY = 1;
79
    /**
80
     * Mutated state
81
     *
82
     * @var int
83
     */
84
    const STATE_MUTATED = 2;
85
    /**
86
     * Published state
87
     *
88
     * @var int
89
     */
90
    const STATE_PUBLISHED = 4;
91
    /**
92
     * Object payload
93
     *
94
     * @var string
95
     */
96
    protected $payload;
97
    /**
98
     * Repository path
99
     *
100
     * @var RepositoryPathInterface
101
     */
102
    protected $path;
103
    /**
104
     * Latest revision index
105
     *
106
     * @var Revision
107
     */
108
    protected $latestRevision;
109
    /**
110
     * Object state
111
     *
112
     * @var int
113
     */
114
    protected $state = self::STATE_CLEAN;
115
    /**
116
     * Property collection states
117
     *
118
     * @var array
119
     */
120
    protected $collectionStates = [];
121
122
    /**
123
     * Object constructor
124
     *
125
     * @param string $payload Object payload
126
     * @param array $propertyData Property data
127
     * @param RepositoryPathInterface $path Object repository path
128
     */
129 20
    public function __construct($payload = '', array $propertyData = [], RepositoryPathInterface $path = null)
130
    {
131
        // If the domain property collection class is invalid
132 20
        if (!$this->domainPropertyCClass
133 20
            || !class_exists($this->domainPropertyCClass)
134 20
            || !(new \ReflectionClass($this->domainPropertyCClass))->isSubclassOf(AbstractDomainProperties::class)
135
        ) {
136 1
            throw new PropertyInvalidArgumentException(
137
                sprintf(
138 1
                    'Invalid domain property collection class "%s"',
139 1
                    $this->domainPropertyCClass
140
                ),
141 1
                PropertyInvalidArgumentException::INVALID_DOMAIN_PROPERTY_COLLECTION_CLASS
142
            );
143
        }
144
145
        // Right after instantiation it's always the current revision
146 19
        $this->path = $path->setRevision(Revision::current());
0 ignored issues
show
Bug introduced by
It seems like $path is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
147
148
        // Load the current revision data
149 19
        $this->loadRevisionData($payload, $propertyData);
150
151
        // Save the latest revision index
152 18
        $this->latestRevision = $this->getRevision();
153 18
    }
154
155
    /**
156
     * Load object revision data
157
     *
158
     * @param string $payload Object payload
159
     * @param array $propertyData Property data
160
     */
161 19
    protected function loadRevisionData($payload = '', array $propertyData = [])
162
    {
163 19
        $this->payload = $payload;
164
165
        // Instantiate the system properties
166 19
        $systemPropertyData = (empty($propertyData[SystemProperties::COLLECTION]) ||
167 19
            !is_array(
168 19
                $propertyData[SystemProperties::COLLECTION]
169 19
            )) ? [] : $propertyData[SystemProperties::COLLECTION];
170 19
        $this->systemProperties = Kernel::create(SystemProperties::class, [$systemPropertyData, $this]);
171
172
        // Instantiate the meta properties
173 18
        $metaPropertyData = (empty($propertyData[MetaProperties::COLLECTION]) ||
174 17
            !is_array(
175 18
                $propertyData[MetaProperties::COLLECTION]
176 18
            )) ? [] : $propertyData[MetaProperties::COLLECTION];
177
        /** @var MetaProperties $metaPropertyCollection */
178 18
        $metaPropertyCollection = Kernel::create(MetaProperties::class, [$metaPropertyData, $this]);
179 18
        $this->setMetaProperties($metaPropertyCollection, true);
180
181
        // Instantiate the domain properties
182 18
        $domainPropertyData = (empty($propertyData[AbstractDomainProperties::COLLECTION]) ||
183 17
            !is_array(
184 18
                $propertyData[AbstractDomainProperties::COLLECTION]
185 18
            )) ? [] : $propertyData[AbstractDomainProperties::COLLECTION];
186
        /** @var AbstractDomainProperties $domainPropertyCollection */
187 18
        $domainPropertyCollection = Kernel::create($this->domainPropertyCClass, [$domainPropertyData, $this]);
188 18
        $this->setDomainProperties($domainPropertyCollection, true);
189
190
        // Instantiate the processing instructions
191 18
        $procInstData = (empty($propertyData[ProcessingInstructions::COLLECTION]) ||
192 14
            !is_array(
193 18
                $propertyData[ProcessingInstructions::COLLECTION]
194 18
            )) ? [] : $propertyData[ProcessingInstructions::COLLECTION];
195
        /** @var ProcessingInstructions $procInstCollection */
196 18
        $procInstCollection = Kernel::create(ProcessingInstructions::class, [$procInstData, $this]);
197 18
        $this->setProcessingInstructions($procInstCollection, true);
198
199
        // Instantiate the object relations
200 18
        $relationData = (empty($propertyData[Relations::COLLECTION]) ||
201 17
            !is_array(
202 18
                $propertyData[Relations::COLLECTION]
203 18
            )) ? [] : $propertyData[Relations::COLLECTION];
204
        /** @var Relations $relationCollection */
205 18
        $relationCollection = Kernel::create(Relations::class, [$relationData, $this]);
206 18
        $this->setRelations($relationCollection, true);
207
208
        // Reset the object state to clean
209 18
        $this->state = self::STATE_CLEAN;
210 18
    }
211
212
    /**
213
     * Return whether the object is in mutated state
214
     *
215
     * @return boolean Mutated state
216
     */
217 3
    public function isMutated()
218
    {
219 3
        return !!($this->state & self::STATE_MUTATED);
220
    }
221
222
    /**
223
     * Use a specific object revision
224
     *
225
     * @param Revision $revision Revision to be used
226
     * @return ObjectInterface Object
227
     * @throws OutOfBoundsException If the requested revision is invalid
228
     */
229 17
    public function useRevision(Revision $revision)
230
    {
231 17
        $isCurrentRevision = false;
232
233
        // If the requested revision is invalid
234 17
        if (!$revision->isCurrent() &&
235 17
            (($revision->getRevision() < 1) || ($revision->getRevision() > $this->latestRevision->getRevision()))
236
        ) {
237
            throw new OutOfBoundsException(sprintf('Invalid object revision "%s"', $revision->getRevision()),
238
                OutOfBoundsException::INVALID_OBJECT_REVISION);
239
        }
240
241
        // If the current revision got requested
242 17
        if ($revision->isCurrent()) {
243 17
            $isCurrentRevision = true;
244 17
            $revision = $this->latestRevision;
245
        }
246
247
        // If the requested revision is not already used
248 17
        if ($revision != $this->getRevision()) {
249
            /** @var ManagerInterface $objectManager */
250
            $objectManager = Kernel::create(Service::class)->getObjectManager();
251
252
            // Load the requested object revision resource
253
            /** @var Revision $newRevision */
254
            $newRevision = $isCurrentRevision ? Revision::current() : $revision;
255
            /** @var RepositoryPath $newRevisionPath */
256
            $newRevisionPath = $this->path->setRevision($newRevision);
257
            $revisionResource = $objectManager->loadObject($newRevisionPath);
258
259
            // Load the revision resource data
260
            $this->loadRevisionData($revisionResource->getPayload(), $revisionResource->getPropertyData());
261
262
            // Set the current revision path
263
            $this->path = $newRevisionPath;
264
        }
265
266 17
        return $this;
267
    }
268
269
    /**
270
     * Return the object repository path
271
     *
272
     * @return RepositoryPathInterface Object repository path
273
     */
274 18
    public function getRepositoryPath()
275
    {
276 18
        return $this->path;
277
    }
278
279
    /**
280
     * Return the object property data
281
     *
282
     * @return array Object property data
283
     */
284 5
    public function getPropertyData()
285
    {
286 5
        $propertyData = array_filter([
287 5
            SystemProperties::COLLECTION => $this->systemProperties->toArray(),
288 5
            MetaProperties::COLLECTION => $this->metaProperties->toArray(),
289 5
            AbstractDomainProperties::COLLECTION => $this->domainProperties->toArray(),
290 5
            ProcessingInstructions::COLLECTION => $this->processingInstructions->toArray(),
291 5
            Relations::COLLECTION => $this->relations->toArray(),
292 5
        ], function (array $collection) {
293 5
            return (boolean)count($collection);
294 5
        });
295
296 5
        return $propertyData;
297
    }
298
299
    /**
300
     * Return the object payload
301
     *
302
     * @return string Object payload
303
     */
304 2
    public function getPayload()
305
    {
306 2
        return $this->payload;
307
    }
308
309
    /**
310
     * Set the payload
311
     *
312
     * @param string $payload Payload
313
     * @return ObjectInterface Self reference
314
     */
315 1
    public function setPayload($payload)
316
    {
317
        // If the payload is changed
318 1
        if ($payload !== $this->payload) {
319 1
            $this->setMutatedState();
320
        }
321
322 1
        $this->payload = $payload;
323 1
        return $this;
324
    }
325
326
    /**
327
     * Set the object state to mutated
328
     */
329 3
    protected function setMutatedState()
330
    {
331
        // If this object is not in mutated state yet
332 3
        if (!($this->state & self::STATE_MUTATED) && !$this->isDraft()) {
333
            // TODO: Send signal
334 3
            $this->convertToDraft();
335
        }
336
337
        // Enable the mutated (and dirty) state
338 3
        $this->state |= (self::STATE_DIRTY | self::STATE_MUTATED);
339 3
    }
340
341
    /**
342
     * Return the object draft mode
343
     *
344
     * @return boolean Object draft mode
345
     */
346 4
    public function isDraft()
347
    {
348 4
        return $this->systemProperties->isDraft() || $this->isPublished();
349
    }
350
351
    /**
352
     * Return whether the object is in published state
353
     *
354
     * @return boolean Published state
355
     */
356 4
    public function isPublished()
357
    {
358 4
        return !!($this->state & self::STATE_PUBLISHED);
359
    }
360
361
    /**
362
     * Convert this object revision into a draft
363
     */
364 3
    protected function convertToDraft()
365
    {
366
        // Increment the latest revision number
367 3
        $this->latestRevision = $this->latestRevision->increment();
368
369
        // Create draft system properties
370 3
        $this->systemProperties = $this->systemProperties->createDraft($this->latestRevision);
371
372
        // Adapt the system properties collection state
373 3
        $this->collectionStates[SystemProperties::COLLECTION] = spl_object_hash($this->systemProperties);
374
375
        // Set the draft flag on the repository path
376 3
        $this->path = $this->path->setDraft(true)->setRevision(Revision::current());
377
378
        // If this is not already a draft ...
379
        // Recreate the system properties
380
        // Copy the object ID
381
        // Copy the object type
382
        // Set the revision number to latest revision + 1
383
        // Set the creation date to now
384
        // Set no publication date
385
        // Set the draft flag on the repository path
386
        // Increase the latest revision by 1
387
388
        // Else if this is a draft
389
        // No action needed
390 3
    }
391
392
    /**
393
     * Return the absolute object URL
394
     *
395
     * @return string
396
     */
397 4
    public function getAbsoluteUrl()
398
    {
399 4
        return getenv('APPARAT_BASE_URL').ltrim($this->path->getRepository()->getUrl(), '/').strval($this->path);
400
    }
401
402
    /**
403
     * Get a processing instruction
404
     *
405
     * @param string $procInst Processing instruction name
406
     * @return mixed Processing instruction
407
     */
408
    public function getProcessingInstruction($procInst)
409
    {
410
        return $this->processingInstructions->getProperty($procInst);
411
    }
412
413
    /**
414
     * Set a processing instruction
415
     *
416
     * @param string $procInst Processing instruction name
417
     * @param mixed $value Processing instruction
418
     * @return ObjectInterface Self reference
419
     */
420 1
    public function setProcessingInstruction($procInst, $value)
421
    {
422 1
        $this->setProcessingInstructions($this->processingInstructions->setProperty($procInst, $value));
423 1
        return $this;
424
    }
425
426
    /**
427
     * Persist the current object revision
428
     *
429
     * @return ObjectInterface Object
430
     */
431 1
    public function persist()
432
    {
433
        // If this is not the latest revision
434 1
        if ($this->getRevision() != $this->latestRevision) {
435
            throw new RuntimeException(
436
                sprintf(
437
                    'Cannot persist revision %s/%s',
438
                    $this->getRevision()->getRevision(),
439
                    $this->latestRevision->getRevision()
440
                ),
441
                RuntimeException::CANNOT_PERSIST_EARLIER_REVISION
442
            );
443
        }
444
445
        // Update the object repository
446 1
        $this->path->getRepository()->updateObject($this);
447
448
        // Reset state
449 1
        $this->state = self::STATE_CLEAN;
450
451 1
        return $this;
452
    }
453
454
    /**
455
     * Publish the current object revision
456
     *
457
     * @return ObjectInterface Object
458
     */
459 1
    public function publish()
460
    {
461
        // If this is a draft
462 1
        if ($this->isDraft()) {
463
            // TODO: Send signal
464
465
            // Create draft system properties
466 1
            $this->systemProperties = $this->systemProperties->publish();
467
468
            // Adapt the system properties collection state
469 1
            $this->collectionStates[SystemProperties::COLLECTION] = spl_object_hash($this->systemProperties);
470
471
            // Set the draft flag on the repository path
472 1
            $this->path = $this->path->setDraft(false);
473
474
            // Flag this object as dirty
475 1
            $this->setPublishedState();
476
        }
477
478 1
        return $this;
479
    }
480
481
    /**
482
     * Set the object state to published
483
     */
484 1
    protected function setPublishedState()
485
    {
486
        // If this object is not in dirty state yet
487 1
        if (!($this->state & self::STATE_PUBLISHED)) {
488
            // TODO: Send signal
489
        }
490
491
        // Enable the dirty state
492 1
        $this->state |= (self::STATE_DIRTY | self::STATE_PUBLISHED);
493 1
    }
494
495
    /**
496
     * Return whether the object is in dirty state
497
     *
498
     * @return boolean Dirty state
499
     */
500 3
    public function isDirty()
501
    {
502 3
        return !!($this->state & self::STATE_DIRTY);
503
    }
504
505
    /**
506
     * Set the object state to dirty
507
     */
508 1
    protected function setDirtyState()
509
    {
510
        // If this object is not in dirty state yet
511 1
        if (!($this->state & self::STATE_DIRTY)) {
512
            // TODO: Send signal
513
        }
514
515
        // Enable the dirty state
516 1
        $this->state |= self::STATE_DIRTY;
517 1
    }
518
}
519