Completed
Push — master ( 1629d3...30b37e )
by Joschi
05:03
created

FileAdapterStrategy::publishObject()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 31
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 4

Importance

Changes 5
Bugs 0 Features 2
Metric Value
cc 4
eloc 16
nc 6
nop 1
dl 0
loc 31
ccs 18
cts 18
cp 1
crap 4
rs 8.5806
c 5
b 0
f 2
1
<?php
2
3
/**
4
 * apparat-object
5
 *
6
 * @category    Apparat
7
 * @package     Apparat\Object
8
 * @subpackage  Apparat\Object\Infrastructure
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\Infrastructure\Repository;
38
39
use Apparat\Kernel\Ports\Kernel;
40
use Apparat\Object\Application\Repository\AbstractAdapterStrategy;
41
use Apparat\Object\Domain\Model\Object\Id;
42
use Apparat\Object\Domain\Model\Object\ObjectInterface;
43
use Apparat\Object\Domain\Model\Object\ResourceInterface;
44
use Apparat\Object\Domain\Model\Object\Revision;
45
use Apparat\Object\Domain\Model\Path\PathInterface;
46
use Apparat\Object\Domain\Model\Path\RepositoryPath;
47
use Apparat\Object\Domain\Model\Path\RepositoryPathInterface;
48
use Apparat\Object\Domain\Repository\AdapterStrategyInterface;
49
use Apparat\Object\Domain\Repository\RepositoryInterface;
50
use Apparat\Object\Domain\Repository\RuntimeException;
51
use Apparat\Object\Domain\Repository\Selector;
52
use Apparat\Object\Domain\Repository\SelectorInterface;
53
use Apparat\Object\Infrastructure\Factory\ResourceFactory;
54
use Apparat\Resource\Infrastructure\Io\File\AbstractFileReaderWriter;
55
use Apparat\Resource\Infrastructure\Io\File\Writer;
56
57
/**
58
 * File adapter strategy
59
 *
60
 * @package Apparat\Object
61
 * @subpackage Apparat\Object\Infrastructure
62
 */
63
class FileAdapterStrategy extends AbstractAdapterStrategy
64
{
65
    /**
66
     * Adapter strategy type
67
     *
68
     * @var string
69
     */
70
    const TYPE = 'file';
71
    /**
72
     * Configuration
73
     *
74
     * @var array
75
     */
76
    protected $config = null;
77
    /**
78
     * Root directory (without trailing directory separator)
79
     *
80
     * @var string
81
     */
82
    protected $root = null;
83
    /**
84
     * Configuration directory (including trailing directory separator)
85
     *
86
     * @var string
87
     */
88
    protected $configDir = null;
89
    /**
90
     * Glob visibilities
91
     *
92
     * @var array
93
     */
94
    protected static $globVisibilities = [
95
        SelectorInterface::VISIBLE => '',
96
        SelectorInterface::HIDDEN => '.',
97
        SelectorInterface::ALL => '{.,}',
98
    ];
99
100
    /**
101
     * Adapter strategy constructor
102
     *
103
     * @param array $config Adapter strategy configuration
104
     * @throws InvalidArgumentException If the root directory configuration is empty
105
     * @throws InvalidArgumentException If the root directory configuration is invalid
106
     */
107 17
    public function __construct(array $config)
108
    {
109 17
        parent::__construct($config, ['root']);
110
111
        // If the root directory configuration is empty
112 16
        if (empty($this->config['root'])) {
113 1
            throw new InvalidArgumentException(
114 1
                'Empty file adapter strategy root',
115
                InvalidArgumentException::EMTPY_FILE_STRATEGY_ROOT
116 1
            );
117
        }
118
119
        // Get the real path of the root directory
120 15
        $this->root = realpath($this->config['root']);
121
122
        // If the repository should be initialized
123 15
        if (!empty($this->config['init'])
124 15
            && (boolean)$this->config['init']
125 15
            && $this->initializeRepository()
126 13
        ) {
127 3
            $this->root = realpath($this->config['root']);
128 3
        }
129
130
        // If the root directory configuration is still invalid
131 13
        if (empty($this->root) || !@is_dir($this->root)) {
132 1
            throw new InvalidArgumentException(
133 1
                sprintf(
134 1
                    'Invalid file adapter strategy root "%s"',
135 1
                    $this->config['root']
136 1
                ),
137
                InvalidArgumentException::INVALID_FILE_STRATEGY_ROOT
138 1
            );
139
        }
140
141 12
        $this->configDir = $this->root.DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
142 12
    }
143
144
    /**
145
     * Initialize the repository
146
     *
147
     * @return boolean Success
148
     * @throws RuntimeException If the repository cannot be initialized
149
     * @throws RuntimeException If the repository size descriptor can not be created
150
     */
151 5
    public function initializeRepository()
152
    {
153
        // Successively create the repository directories
154 5
        $repoDirectories = [$this->config['root'], $this->config['root'].DIRECTORY_SEPARATOR.'.repo'];
155 5
        foreach ($repoDirectories as $repoDirectory) {
156
            // If the repository cannot be initialized
157 5
            if (file_exists($repoDirectory) ? !is_dir($repoDirectory) : !mkdir($repoDirectory, 0777, true)) {
158 1
                throw new RuntimeException('Could not initialize repository', RuntimeException::REPO_NOT_INITIALIZED);
159
            }
160 4
        }
161
162
        // If the repository size descriptor can not be created
163 4
        $configDir = $this->config['root'].DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
164 4
        if ((file_exists($configDir.'size.txt') && !is_file($configDir.'size.txt'))
165 3
            || !file_put_contents($configDir.'size.txt', '0')
166 4
        ) {
167 1
            throw new RuntimeException(
168 1
                'Could not create repository size descriptor',
169
                RuntimeException::REPO_SIZE_DESCRIPTOR_NOT_CREATED
170 1
            );
171
        }
172
173 3
        return true;
174
    }
175
176
    /**
177
     * Find objects by selector
178
     *
179
     * @param Selector|SelectorInterface $selector Object selector
180
     * @param RepositoryInterface $repository Object repository
181
     * @return PathInterface[] Object paths
182
     */
183 7
    public function findObjectPaths(SelectorInterface $selector, RepositoryInterface $repository)
184
    {
185 7
        chdir($this->root);
186
187
        // Build a glob string from the selector
188 7
        $glob = '';
189 7
        $globFlags = GLOB_ONLYDIR | GLOB_NOSORT;
190
191 7
        $year = $selector->getYear();
192 7
        if ($year !== null) {
193 7
            $glob .= '/'.$year;
194 7
        }
195
196 7
        $month = $selector->getMonth();
197 7
        if ($month !== null) {
198 7
            $glob .= '/'.$month;
199 7
        }
200
201 7
        $day = $selector->getDay();
202 7
        if ($day !== null) {
203 7
            $glob .= '/'.$day;
204 7
        }
205
206 7
        $hour = $selector->getHour();
207 7
        if ($hour !== null) {
208 2
            $glob .= '/'.$hour;
209 2
        }
210
211 7
        $minute = $selector->getMinute();
212 7
        if ($minute !== null) {
213 2
            $glob .= '/'.$minute;
214 2
        }
215
216 7
        $second = $selector->getSecond();
217 7
        if ($second !== null) {
218 2
            $glob .= '/'.$second;
219 2
        }
220
221 7
        $visibility = $selector->getVisibility();
222 7
        $uid = $selector->getId();
223 7
        $type = $selector->getType();
224 7
        if (($uid !== null) || ($type !== null)) {
225 7
            $glob .= '/'.($uid ?: SelectorInterface::WILDCARD).'.'.($type ?: SelectorInterface::WILDCARD);
226
227 7
            $revision = $selector->getRevision();
228 7
            if ($revision !== null) {
229 1
                $glob .= '/'.self::$globVisibilities[$visibility].($uid ?: SelectorInterface::WILDCARD).'-'.$revision;
230 1
                $globFlags &= ~GLOB_ONLYDIR;
231 1
            }
232 7
        }
233
234 7
        return array_map(
235 7
            function ($objectPath) use ($repository) {
236 7
                return Kernel::create(RepositoryPath::class, [$repository, '/'.$objectPath]);
237 7
            },
238 7
            glob(ltrim($glob, '/'), $globFlags)
239 7
        );
240
    }
241
242
    /**
243
     * Test if an object resource exists
244
     *
245
     * @param string $resourcePath Repository relative resource path
246
     * @return boolean Object resource exists
247
     */
248
    public function hasObjectResource($resourcePath)
249
    {
250
//        echo 'testing '.$this->root.$resourcePath;
251
        return is_file($this->root.$resourcePath);
252
    }
253
254
    /**
255
     * Find and return an object resource
256
     *
257
     * @param string $resourcePath Repository relative resource path
258
     * @return ResourceInterface Object resource
259
     */
260 25
    public function getObjectResource($resourcePath)
261
    {
262 25
        return ResourceFactory::createFromSource(AbstractFileReaderWriter::WRAPPER.$this->root.$resourcePath);
0 ignored issues
show
Bug Best Practice introduced by
The return type of return \Apparat\Object\I...>root . $resourcePath); (Apparat\Resource\Domain\...source\AbstractResource) is incompatible with the return type declared by the interface Apparat\Object\Applicati...face::getObjectResource of type Apparat\Object\Domain\Mo...bject\ResourceInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
263
    }
264
265
    /**
266
     * Allocate an object ID and create an object resource
267
     *
268
     * @param \Closure $creator Object creation closure
269
     * @return ObjectInterface Object
270
     * @throws RuntimeException If no object could be created
271
     * @throws \Exception If another error occurs
272
     */
273 2
    public function createObjectResource(\Closure $creator)
274
    {
275 2
        $sizeDescriptor = null;
276
277
        try {
278
            // Open the size descriptor
279 2
            $sizeDescriptor = fopen($this->configDir.'size.txt', 'r+');
280
281
            // If a lock of the size descriptor can be acquired
282 2
            if (flock($sizeDescriptor, LOCK_EX)) {
283
                // Determine the current repository size
284 1
                $repositorySize = '';
285 1
                while (!feof($sizeDescriptor)) {
286 1
                    $repositorySize .= fread($sizeDescriptor, 8);
287 1
                }
288 1
                $repositorySize = intval(trim($repositorySize));
289
290
                // Instantiate the next consecutive object ID
291 1
                $nextObjectId = Kernel::create(Id::class, [++$repositorySize]);
292
293
                // Create & persist the object (bypassing the repository)
294 1
                $object = $creator($nextObjectId);
295 1
                $this->persistObject($object);
296
297
                // Dump the new repository size, unlock the size descriptor
298 1
                ftruncate($sizeDescriptor, 0);
299 1
                fwrite($sizeDescriptor, $repositorySize);
300 1
                fflush($sizeDescriptor);
301 1
                flock($sizeDescriptor, LOCK_UN);
302
303
                // Return the newly created object
304 1
                return $object;
305
            }
306
307
            // If no object could be created
308 1
            throw new RuntimeException(
309 1
                'The repository size descriptor is unlockable',
310
                RuntimeException::REPO_SIZE_DESCRIPTOR_UNLOCKABLE
311 1
            );
312
313
            // If any exception is thrown
314 1
        } catch (\Exception $e) {
315
            // Release the size descriptor lock
316 1
            if (is_resource($sizeDescriptor)) {
317 1
                flock($sizeDescriptor, LOCK_UN);
318 1
            }
319
320
            // Forward the thrown exception
321 1
            throw $e;
322
        }
323
    }
324
325
    /**
326
     * Persist an object in the repository
327
     *
328
     * @param ObjectInterface $object Object
329
     * @return AdapterStrategyInterface Self reference
330
     */
331 1
    public function persistObject(ObjectInterface $object)
332
    {
333
        // If the object has just been deleted
334 1
        if ($object->hasBeenDeleted()) {
335 1
            return $this->deleteObject($object);
336
337
            // Elseif the object has just been undeleted
338 1
        } elseif ($object->hasBeenUndeleted()) {
339
            return $this->undeleteObject($object);
340
341
            // If the object has just been published
342 1
        } elseif ($object->hasBeenPublished()) {
343 1
            $this->publishObject($object);
344 1
        }
345
346
        // Persist the object resource
347 1
        return $this->persistObjectResource($object);
348
    }
349
350
    /**
351
     * Publish an object in the repository
352
     *
353
     * @param ObjectInterface $object
354
     */
355 1
    protected function publishObject(ObjectInterface $object)
356
    {
357 1
        $objectRepositoryPath = $object->getRepositoryPath();
358
359
        // If the object had been persisted as a draft: Remove the draft resource
360 1
        $objectDraftPath = $objectRepositoryPath->setRevision($object->getRevision()->setDraft(true));
361 1
        $absObjectDraftPath = $this->absoluteResourcePath($objectDraftPath);
362 1
        if (@file_exists($absObjectDraftPath)) {
363 1
            unlink($absObjectDraftPath);
364 1
        }
365
366
        // If it's not the first object revision: Rotate the previous revision resource
367 1
        $objectRevisionNumber = $object->getRevision()->getRevision();
368 1
        if ($objectRevisionNumber > 1) {
369
            // Build the "current" object repository path
370 1
            $currentRevision = Revision::current();
371
            $curObjectResPath =
372 1
                $this->absoluteResourcePath($objectRepositoryPath->setRevision($currentRevision));
373
374
            // Build the previous object repository path
375
            /** @var Revision $previousRevision */
376 1
            $previousRevision = Kernel::create(Revision::class, [$objectRevisionNumber - 1]);
377
            $prevObjectResPath
378 1
                = $this->absoluteResourcePath($objectRepositoryPath->setRevision($previousRevision));
379
380
            // Rotate the previous revision's resource path
381 1
            if (file_exists($curObjectResPath)) {
382 1
                rename($curObjectResPath, $prevObjectResPath);
383 1
            }
384 1
        }
385 1
    }
386
387
    /**
388
     * Build an absolute repository resource path
389
     *
390
     * @param RepositoryPathInterface $repositoryPath Repository path
391
     * @return string Absolute repository resource path
392
     */
393 1
    protected function absoluteResourcePath(RepositoryPathInterface $repositoryPath)
394
    {
395 1
        return $this->root.str_replace(
396 1
            '/',
397 1
            DIRECTORY_SEPARATOR,
398 1
            $repositoryPath->withExtension(getenv('OBJECT_RESOURCE_EXTENSION'))
399 1
        );
400
    }
401
402
    /**
403
     * Persist an object resource in the repository
404
     *
405
     * @param ObjectInterface $object Object
406
     * @return AdapterStrategyInterface Self reference
407
     */
408 1
    protected function persistObjectResource(ObjectInterface $object)
409
    {
410
        /** @var \Apparat\Object\Infrastructure\Model\Object\Resource $objectResource */
411 1
        $objectResource = ResourceFactory::createFromObject($object);
412
413
        // Create the absolute object resource path
414 1
        $objectResourcePath = $this->absoluteResourcePath($object->getRepositoryPath());
415
416
        /** @var Writer $fileWriter */
417 1
        $fileWriter = Kernel::create(
418 1
            Writer::class,
419 1
            [$objectResourcePath, Writer::FILE_CREATE | Writer::FILE_CREATE_DIRS | Writer::FILE_OVERWRITE]
420 1
        );
421 1
        $objectResource->dump($fileWriter);
422
423 1
        return $this;
424
    }
425
426
    /**
427
     * Return the repository size (number of objects in the repository)
428
     *
429
     * @return int Repository size
430
     */
431 2
    public function getRepositorySize()
432
    {
433 2
        $sizeDescriptorFile = $this->configDir.'size.txt';
434 2
        $repositorySize = 0;
435 2
        if (is_file($sizeDescriptorFile) && is_readable($sizeDescriptorFile)) {
436 2
            $repositorySize = intval(file_get_contents($this->configDir.'size.txt'));
437 2
        }
438 2
        return $repositorySize;
439
    }
440
441
    /**
442
     * Delete all revisions of an object
443
     *
444
     * @param ObjectInterface $object Object
445
     * @return ObjectInterface Object
446
     */
447 1
    protected function deleteObject(ObjectInterface $object)
448
    {
449
        // Delete all object revisions
450
        /** @var ObjectInterface $objectRevision */
451 1
        foreach ($object as $objectRevision) {
0 ignored issues
show
Bug introduced by
The expression $object of type object<Apparat\Object\Do...Object\ObjectInterface> is not traversable.
Loading history...
452 1
            $this->persistObjectResource($objectRevision->delete());
453 1
        }
454
455
        // TODO: Delete object resource directory
456
457 1
        return $this;
458
    }
459
460
    /**
461
     * Undelete all revisions of an object
462
     *
463
     * @param ObjectInterface $object Object
464
     * @return ObjectInterface Object
465
     */
466
    protected function undeleteObject(ObjectInterface $object)
467
    {
468
        // Undelete all object revisions
469
        /** @var ObjectInterface $objectRevision */
470
        foreach ($object as $objectRevision) {
0 ignored issues
show
Bug introduced by
The expression $object of type object<Apparat\Object\Do...Object\ObjectInterface> is not traversable.
Loading history...
471
            $this->persistObjectResource($objectRevision->undelete());
472
        }
473
474
        // TODO: Undelete object resource directory
475
476
        return $this;
477
    }
478
}
479