Completed
Push — master ( abf23a...b0f4e6 )
by Joschi
05:13
created

AbstractObject::updatePath()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 9
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 7
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 3
eloc 6
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 9
ccs 7
cts 7
cp 1
crap 3
rs 9.6666
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\StatesTrait;
47
use Apparat\Object\Domain\Model\Object\Traits\SystemPropertiesTrait;
48
use Apparat\Object\Domain\Model\Path\RepositoryPath;
49
use Apparat\Object\Domain\Model\Path\RepositoryPathInterface;
50
use Apparat\Object\Domain\Model\Properties\AbstractDomainProperties;
51
use Apparat\Object\Domain\Model\Properties\InvalidArgumentException as PropertyInvalidArgumentException;
52
use Apparat\Object\Domain\Model\Properties\MetaProperties;
53
use Apparat\Object\Domain\Model\Properties\ProcessingInstructions;
54
use Apparat\Object\Domain\Model\Properties\Relations;
55
use Apparat\Object\Domain\Model\Properties\SystemProperties;
56
use Apparat\Object\Domain\Repository\Service;
57
58
/**
59
 * Abstract object
60
 *
61
 * @package Apparat\Object
62
 * @subpackage Apparat\Object\Domain
63
 */
64
abstract class AbstractObject implements ObjectInterface, \Iterator, \Countable
65
{
66
    /**
67
     * Use traits
68
     */
69
    use SystemPropertiesTrait, MetaPropertiesTrait, DomainPropertiesTrait, RelationsTrait,
70
        ProcessingInstructionsTrait, PayloadTrait, IterableTrait, StatesTrait;
71
72
    /**
73
     * Repository path
74
     *
75
     * @var RepositoryPathInterface
76
     */
77
    protected $path;
78
    /**
79
     * Latest revision
80
     *
81
     * @var Revision
82
     */
83
    protected $latestRevision;
84
85
    /**
86
     * Object constructor
87
     *
88
     * @param RepositoryPathInterface $path Object repository path
89
     * @param string $payload Object payload
90
     * @param array $propertyData Property data
91
     */
92 26
    public function __construct(RepositoryPathInterface $path, $payload = '', array $propertyData = [])
93
    {
94
        // If the domain property collection class is invalid
95 26
        if (!$this->domainPropertyCClass
96 26
            || !class_exists($this->domainPropertyCClass)
97 26
            || !(new \ReflectionClass($this->domainPropertyCClass))->isSubclassOf(AbstractDomainProperties::class)
98
        ) {
99 1
            throw new PropertyInvalidArgumentException(
100
                sprintf(
101 1
                    'Invalid domain property collection class "%s"',
102 1
                    $this->domainPropertyCClass
103
                ),
104 1
                PropertyInvalidArgumentException::INVALID_DOMAIN_PROPERTY_COLLECTION_CLASS
105
            );
106
        }
107
108
        // Save the original object path
109 25
        $this->path = $path;
110
111
        // Load the current revision data
112 25
        $this->loadRevisionData($payload, $propertyData);
113
114
        // Determine the latest revision number (considering a possible draft)
115 24
        $this->latestRevision = $this->hasDraft()
116
            ? Kernel::create(Revision::class, [$this->getRevision()->getRevision() + 1, true])
117 24
            : $this->getRevision();
118
119
        // Update the object path
120 24
        $this->updatePath();
121 24
    }
122
123
    /**
124
     * Load object revision data
125
     *
126
     * @param string $payload Object payload
127
     * @param array $propertyData Property data
128
     */
129 25
    protected function loadRevisionData($payload = '', array $propertyData = [])
130
    {
131 25
        $this->payload = $payload;
132
133
        // Instantiate the system properties
134 25
        $systemPropertyData = (empty($propertyData[SystemProperties::COLLECTION]) ||
135 25
            !is_array(
136 25
                $propertyData[SystemProperties::COLLECTION]
137 25
            )) ? [] : $propertyData[SystemProperties::COLLECTION];
138 25
        $this->systemProperties = Kernel::create(SystemProperties::class, [$systemPropertyData, $this]);
139
140
        // Instantiate the meta properties
141 24
        $metaPropertyData = (empty($propertyData[MetaProperties::COLLECTION]) ||
142 23
            !is_array(
143 24
                $propertyData[MetaProperties::COLLECTION]
144 24
            )) ? [] : $propertyData[MetaProperties::COLLECTION];
145
        /** @var MetaProperties $metaProperties */
146 24
        $metaProperties = Kernel::create(MetaProperties::class, [$metaPropertyData, $this]);
147 24
        $this->setMetaProperties($metaProperties, true);
148
149
        // Instantiate the domain properties
150 24
        $domainPropertyData = (empty($propertyData[AbstractDomainProperties::COLLECTION]) ||
151 23
            !is_array(
152 24
                $propertyData[AbstractDomainProperties::COLLECTION]
153 24
            )) ? [] : $propertyData[AbstractDomainProperties::COLLECTION];
154
        /** @var AbstractDomainProperties $domainProperties */
155 24
        $domainProperties = Kernel::create($this->domainPropertyCClass, [$domainPropertyData, $this]);
156 24
        $this->setDomainProperties($domainProperties, true);
157
158
        // Instantiate the processing instructions
159 24
        $procInstData = (empty($propertyData[ProcessingInstructions::COLLECTION]) ||
160 20
            !is_array(
161 24
                $propertyData[ProcessingInstructions::COLLECTION]
162 24
            )) ? [] : $propertyData[ProcessingInstructions::COLLECTION];
163
        /** @var ProcessingInstructions $procInstCollection */
164 24
        $procInstCollection = Kernel::create(ProcessingInstructions::class, [$procInstData, $this]);
165 24
        $this->setProcessingInstructions($procInstCollection, true);
166
167
        // Instantiate the object relations
168 24
        $relationData = (empty($propertyData[Relations::COLLECTION]) ||
169 23
            !is_array(
170 24
                $propertyData[Relations::COLLECTION]
171 24
            )) ? [] : $propertyData[Relations::COLLECTION];
172
        /** @var Relations $relationCollection */
173 24
        $relationCollection = Kernel::create(Relations::class, [$relationData, $this]);
174 24
        $this->setRelations($relationCollection, true);
175
176
        // Reset the object state
177 24
        $this->resetState();
178 24
    }
179
180
    /**
181
     * Return whether this object already has a draft revision
182
     */
183 24
    protected function hasDraft()
184
    {
185
        // Create the object draft resource path
186 24
        $draftPath = $this->path->setRevision(Revision::current(true));
187
188
        // Use the object manager to look for a draft resource
189
        /** @var ManagerInterface $objectManager */
190 24
        $objectManager = Kernel::create(Service::class)->getObjectManager();
191 24
        return $objectManager->objectResourceExists($draftPath);
192
    }
193
194
    /**
195
     * Update the object path
196
     */
197 24
    protected function updatePath()
198
    {
199 24
        $revision = $this->getRevision();
200 24
        $this->path = $this->path->setRevision(
201 24
            !$revision->isDraft() && ($this->getCurrentRevision()->getRevision() == $revision->getRevision())
202 21
                ? Revision::current($revision->isDraft())
203 24
                : $revision
204
        );
205 24
    }
206
207
    /**
208
     * Return this object's current revision
209
     *
210
     * @return Revision Current revision
211
     */
212 24
    protected function getCurrentRevision()
213
    {
214 24
        if ($this->latestRevision->isDraft() && ($this->latestRevision->getRevision() > 1)) {
215
            return Kernel::create(Revision::class, [$this->latestRevision->getRevision() - 1, false]);
216
        }
217 24
        return $this->latestRevision;
218
    }
219
220
    /**
221
     * Use a specific object revision
222
     *
223
     * @param Revision $revision Revision to be used
224
     * @return ObjectInterface Object
225
     * @throws OutOfBoundsException If a revision beyond the latest one is requested
226
     */
227 23
    public function useRevision(Revision $revision)
228
    {
229
        // If a revision beyond the latest one is requested
230 23
        if (!$revision->isCurrent() && ($revision->getRevision() > $this->latestRevision->getRevision())) {
231
            throw new OutOfBoundsException(
232
                sprintf('Invalid object revision "%s"', $revision->getRevision()),
233
                OutOfBoundsException::INVALID_OBJECT_REVISION
234
            );
235
        }
236
237
        // Determine whether the current revision was requested
238 23
        $currentRevision = $this->getCurrentRevision();
239 23
        $isCurrentRevision = $revision->isCurrent() || $currentRevision->equals($revision);
240 23
        if ($isCurrentRevision) {
241 23
            $revision = $currentRevision;
242
        }
243
244
        // If the requested revision is not already used
245 23
        if (!$this->getRevision()->equals($revision)) {
246
247
            /** @var ManagerInterface $objectManager */
248
            $objectManager = Kernel::create(Service::class)->getObjectManager();
249
            /** @var Revision $newRevision */
250
            $newRevision = $isCurrentRevision ? Revision::current() : $revision;
251
            /** @var RepositoryPath $newRevisionPath */
252
            $newRevisionPath = $this->path->setRevision($newRevision);
253
254
            // Instantiate the requested revision resource
255
            $revisionResource = $objectManager->loadObjectResource($newRevisionPath);
256
257
            // Load the revision resource data
258
            $this->loadRevisionData($revisionResource->getPayload(), $revisionResource->getPropertyData());
259
260
            // Update the object path
261
            $this->updatePath();
262
        }
263
264 23
        return $this;
265
    }
266
267
    /**
268
     * Return the object repository path
269
     *
270
     * @return RepositoryPathInterface Object repository path
271
     */
272 24
    public function getRepositoryPath()
273
    {
274 24
        return $this->path;
275
    }
276
277
    /**
278
     * Return the object property data
279
     *
280
     * @return array Object property data
281
     */
282 5
    public function getPropertyData()
283
    {
284 5
        $propertyData = array_filter([
285 5
            SystemProperties::COLLECTION => $this->systemProperties->toArray(),
286 5
            MetaProperties::COLLECTION => $this->metaProperties->toArray(),
287 5
            AbstractDomainProperties::COLLECTION => $this->domainProperties->toArray(),
288 5
            ProcessingInstructions::COLLECTION => $this->processingInstructions->toArray(),
289 5
            Relations::COLLECTION => $this->relations->toArray(),
290 5
        ], function (array $collection) {
291 5
            return (boolean)count($collection);
292 5
        });
293
294 5
        return $propertyData;
295
    }
296
297
    /**
298
     * Return the absolute object URL
299
     *
300
     * @return string
301
     */
302 4
    public function getAbsoluteUrl()
303
    {
304 4
        return getenv('APPARAT_BASE_URL').ltrim($this->path->getRepository()->getUrl(), '/').strval($this->path);
305
    }
306
307
    /**
308
     * Persist the current object revision
309
     *
310
     * @return ObjectInterface Object
311
     */
312 1
    public function persist()
313
    {
314
        // If this is not the latest revision
315 1
        if ($this->getRevision()->getRevision() !== $this->latestRevision->getRevision()) {
316
            throw new RuntimeException(
317
                sprintf(
318
                    'Cannot persist revision %s/%s',
319
                    $this->getRevision()->getRevision(),
320
                    $this->latestRevision->getRevision()
321
                ),
322
                RuntimeException::CANNOT_PERSIST_EARLIER_REVISION
323
            );
324
        }
325
326
        // Update the object repository
327 1
        $this->path->getRepository()->updateObject($this);
328
329
        // Reset to a clean state
330 1
        $this->resetState();
331 1
        $this->latestRevision = $this->getRevision();
332 1
        $this->updatePath();
333
334 1
        return $this;
335
    }
336
337
    /**
338
     * Publish the current object revision
339
     *
340
     * @return ObjectInterface Object
341
     */
342 1
    public function publish()
343
    {
344
        // If this is an unpublished draft
345 1
        if ($this->isDraft() & !$this->hasBeenPublished()) {
346 1
            $this->setPublishedState();
347 1
            $this->latestRevision = $this->latestRevision->setDraft(false);
348 1
            $this->updatePath();
349
        }
350
351 1
        return $this;
352
    }
353
354
    /**
355
     * Delete the object and all its revisions
356
     *
357
     * @return ObjectInterface Object
358
     */
359
    public function delete()
360
    {
361
        // If this object is not already deleted
362
        if (!$this->isDeleted() && !$this->hasBeenDeleted()) {
363
            $this->setDeletedState();
364
        }
365
366
        return $this;
367
368
    }
369
370
    /**
371
     * Undelete the object and all its revisions
372
     *
373
     * @return ObjectInterface Object
374
     */
375
    public function undelete()
376
    {
377
        // If this object is already deleted
378
        if ($this->isDeleted() && !$this->hasBeenUndeleted()) {
379
            $this->setUndeletedState();
380
        }
381
    }
382
383
    /**
384
     * Convert this object revision into a draft
385
     */
386 4
    protected function convertToDraft()
387
    {
388
        // Assume the latest revision as draft revision
389 4
        $draftRevision = $this->latestRevision;
390
391
        // If it equals the current revision: Spawn a draft
392 4
        if ($draftRevision->equals($this->getCurrentRevision())) {
393 4
            $draftRevision = $this->latestRevision = $draftRevision->increment()->setDraft(true);
394
        }
395
396
        // Set the system properties to draft mode
397 4
        $this->setSystemProperties($this->systemProperties->createDraft($draftRevision), true);
398
399
        // Update the object path
400 4
        $this->updatePath();
401 4
    }
402
}
403