Completed
Push — master ( e40fc7...7c0043 )
by Joschi
03:23
created

FileAdapterStrategy::absoluteResourcePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
cc 1
eloc 5
c 2
b 0
f 1
nc 1
nop 1
dl 0
loc 8
ccs 6
cts 6
cp 1
crap 1
rs 9.4285
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 as DomainRepositoryRuntimeException;
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
use Apparat\Object\Infrastructure\Utilities\File;
57
58
/**
59
 * File adapter strategy
60
 *
61
 * @package Apparat\Object
62
 * @subpackage Apparat\Object\Infrastructure
63
 */
64
class FileAdapterStrategy extends AbstractAdapterStrategy
65
{
66
    /**
67
     * Adapter strategy type
68
     *
69
     * @var string
70
     */
71
    const TYPE = 'file';
72
    /**
73
     * Configuration
74
     *
75
     * @var array
76
     */
77
    protected $config = null;
78
    /**
79
     * Root directory (without trailing directory separator)
80
     *
81
     * @var string
82
     */
83
    protected $root = null;
84
    /**
85
     * Configuration directory (including trailing directory separator)
86
     *
87
     * @var string
88
     */
89
    protected $configDir = null;
90
    /**
91
     * Glob visibilities
92
     *
93
     * @var array
94
     */
95
    protected static $globVisibilities = [
96
        SelectorInterface::VISIBLE => '',
97
        SelectorInterface::HIDDEN => '.',
98
        SelectorInterface::ALL => '{.,}',
99
    ];
100
101
    /**
102
     * Adapter strategy constructor
103
     *
104
     * @param array $config Adapter strategy configuration
105
     * @throws InvalidArgumentException If the root directory configuration is empty
106
     * @throws InvalidArgumentException If the root directory configuration is invalid
107
     */
108 20
    public function __construct(array $config)
109
    {
110 20
        parent::__construct($config, ['root']);
111
112
        // If the root directory configuration is empty
113 19
        if (empty($this->config['root'])) {
114 1
            throw new InvalidArgumentException(
115 1
                'Empty file adapter strategy root',
116 1
                InvalidArgumentException::EMTPY_FILE_STRATEGY_ROOT
117
            );
118
        }
119
120
        // Get the real path of the root directory
121 18
        $this->root = realpath($this->config['root']);
122
123
        // If the repository should be initialized
124 18
        if (!empty($this->config['init'])
125 18
            && (boolean)$this->config['init']
126 18
            && $this->initializeRepository()
127
        ) {
128 6
            $this->root = realpath($this->config['root']);
129
        }
130
131
        // If the root directory configuration is still invalid
132 16
        if (empty($this->root) || !@is_dir($this->root)) {
133 1
            throw new InvalidArgumentException(
134
                sprintf(
135 1
                    'Invalid file adapter strategy root "%s"',
136 1
                    $this->config['root']
137
                ),
138 1
                InvalidArgumentException::INVALID_FILE_STRATEGY_ROOT
139
            );
140
        }
141
142 15
        $this->configDir = $this->root.DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
143 15
    }
144
145
    /**
146
     * Initialize the repository
147
     *
148
     * @return boolean Success
149
     * @throws DomainRepositoryRuntimeException If the repository cannot be initialized
150
     * @throws DomainRepositoryRuntimeException If the repository size descriptor can not be created
151
     */
152 8
    public function initializeRepository()
153
    {
154
        // Successively create the repository directories
155 8
        $repoDirectories = [$this->config['root'], $this->config['root'].DIRECTORY_SEPARATOR.'.repo'];
156 8
        foreach ($repoDirectories as $repoDirectory) {
157
            // If the repository cannot be initialized
158 8
            if (file_exists($repoDirectory) ? !is_dir($repoDirectory) : !mkdir($repoDirectory, 0777, true)) {
159 8
                throw new DomainRepositoryRuntimeException('Could not initialize repository', DomainRepositoryRuntimeException::REPO_NOT_INITIALIZED);
160
            }
161
        }
162
163
        // If the repository size descriptor can not be created
164 7
        $configDir = $this->config['root'].DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
165 7
        if ((file_exists($configDir.'size.txt') && !is_file($configDir.'size.txt'))
166 7
            || !file_put_contents($configDir.'size.txt', '0')
167
        ) {
168 1
            throw new DomainRepositoryRuntimeException(
169 1
                'Could not create repository size descriptor',
170 1
                DomainRepositoryRuntimeException::REPO_SIZE_DESCRIPTOR_NOT_CREATED
171
            );
172
        }
173
174 6
        return true;
175
    }
176
177
    /**
178
     * Find objects by selector
179
     *
180
     * @param Selector|SelectorInterface $selector Object selector
181
     * @param RepositoryInterface $repository Object repository
182
     * @return PathInterface[] Object paths
183
     */
184 7
    public function findObjectPaths(SelectorInterface $selector, RepositoryInterface $repository)
185
    {
186 7
        chdir($this->root);
187
188
        // Build a glob string from the selector
189 7
        $glob = '';
190 7
        $globFlags = GLOB_ONLYDIR | GLOB_NOSORT;
191
192 7
        $year = $selector->getYear();
193 7
        if ($year !== null) {
194 7
            $glob .= '/'.$year;
195
        }
196
197 7
        $month = $selector->getMonth();
198 7
        if ($month !== null) {
199 7
            $glob .= '/'.$month;
200
        }
201
202 7
        $day = $selector->getDay();
203 7
        if ($day !== null) {
204 7
            $glob .= '/'.$day;
205
        }
206
207 7
        $hour = $selector->getHour();
208 7
        if ($hour !== null) {
209 2
            $glob .= '/'.$hour;
210
        }
211
212 7
        $minute = $selector->getMinute();
213 7
        if ($minute !== null) {
214 2
            $glob .= '/'.$minute;
215
        }
216
217 7
        $second = $selector->getSecond();
218 7
        if ($second !== null) {
219 2
            $glob .= '/'.$second;
220
        }
221
222 7
        $visibility = $selector->getVisibility();
223 7
        $uid = $selector->getId();
224 7
        $type = $selector->getType();
225 7
        if (($uid !== null) || ($type !== null)) {
226 7
            $glob .= '/'.($uid ?: SelectorInterface::WILDCARD).'-'.($type ?: SelectorInterface::WILDCARD);
227
228 7
            $revision = $selector->getRevision();
229 7
            if ($revision !== null) {
230 1
                $glob .= '/'.self::$globVisibilities[$visibility].($uid ?: SelectorInterface::WILDCARD).'-'.$revision;
231 1
                $globFlags &= ~GLOB_ONLYDIR;
232
            }
233
        }
234
235 7
        return array_map(
236 7
            function ($objectPath) use ($repository) {
237 7
                return Kernel::create(RepositoryPath::class, [$repository, '/'.$objectPath]);
238 7
            },
239 7
            glob(ltrim($glob, '/'), $globFlags)
240
        );
241
    }
242
243
    /**
244
     * Test if an object resource exists
245
     *
246
     * @param string $resourcePath Repository relative resource path
247
     * @return boolean Object resource exists
248
     */
249 28
    public function hasResource($resourcePath)
250
    {
251 28
        return is_file($this->root.$resourcePath);
252
    }
253
254
    /**
255
     * Return an individual hash for a resource
256
     *
257
     * @param string $resourcePath Repository relative resource path
258
     * @return string|null Resource hash
259
     */
260 1
    public function getResourceHash($resourcePath)
261
    {
262 1
        return $this->hasResource($this->root.$resourcePath) ? File::hash($this->root.$resourcePath) : null;
263
    }
264
265
    /**
266
     * Import a resource into this repository
267
     *
268
     * @param string $source Source resource
269
     * @param string $target Repository relative target resource path
270
     * @return boolean Success
271
     */
272 1
    public function importResource($source, $target)
273
    {
274 1
        return copy($source, $this->root.$target);
275
    }
276
277
    /**
278
     * Find and return an object resource
279
     *
280
     * @param string $resourcePath Repository relative resource path
281
     * @return ResourceInterface Object resource
282
     */
283 26
    public function getObjectResource($resourcePath)
284
    {
285 26
        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...
286
    }
287
288
    /**
289
     * Allocate an object ID and create an object resource
290
     *
291
     * @param \Closure $creator Object creation closure
292
     * @return ObjectInterface Object
293
     * @throws DomainRepositoryRuntimeException If no object could be created
294
     * @throws \Exception If another error occurs
295
     */
296 5
    public function createObjectResource(\Closure $creator)
297
    {
298 5
        $sizeDescriptor = null;
299
300
        try {
301
            // Open the size descriptor
302 5
            $sizeDescriptor = fopen($this->configDir.'size.txt', 'r+');
303
304
            // If a lock of the size descriptor can be acquired
305 5
            if (flock($sizeDescriptor, LOCK_EX)) {
306
                // Determine the current repository size
307 4
                $repositorySize = '';
308 4
                while (!feof($sizeDescriptor)) {
309 4
                    $repositorySize .= fread($sizeDescriptor, 8);
310
                }
311 4
                $repositorySize = intval(trim($repositorySize));
312
313
                // Instantiate the next consecutive object ID
314 4
                $nextObjectId = Kernel::create(Id::class, [++$repositorySize]);
315
316
                // Create & persist the object (bypassing the repository)
317 4
                $object = $creator($nextObjectId);
318 4
                $this->persistObject($object);
319
320
                // Dump the new repository size, unlock the size descriptor
321 4
                ftruncate($sizeDescriptor, 0);
322 4
                fwrite($sizeDescriptor, $repositorySize);
323 4
                fflush($sizeDescriptor);
324 4
                flock($sizeDescriptor, LOCK_UN);
325
326
                // Return the newly created object
327 4
                return $object;
328
            }
329
330
            // If no object could be created
331 1
            throw new DomainRepositoryRuntimeException(
332 1
                'The repository size descriptor is unlockable',
333 1
                DomainRepositoryRuntimeException::REPO_SIZE_DESCRIPTOR_UNLOCKABLE
334
            );
335
336
            // If any exception is thrown
337 1
        } catch (\Exception $e) {
338
            // Release the size descriptor lock
339 1
            if (is_resource($sizeDescriptor)) {
340 1
                flock($sizeDescriptor, LOCK_UN);
341
            }
342
343
            // Forward the thrown exception
344 1
            throw $e;
345
        }
346
    }
347
348
    /**
349
     * Persist an object in the repository
350
     *
351
     * @param ObjectInterface $object Object
352
     * @return AdapterStrategyInterface Self reference
353
     */
354 4
    public function persistObject(ObjectInterface $object)
355
    {
356
        // If the object has just been deleted
357 4
        if ($object->hasBeenDeleted()) {
358 3
            return $this->deleteObject($object);
359
360
            // Elseif the object has just been undeleted
361 4
        } elseif ($object->hasBeenUndeleted()) {
362 2
            return $this->undeleteObject($object);
363
364
            // If the object has just been published
365 4
        } elseif ($object->hasBeenPublished()) {
366 2
            $this->publishObject($object);
367
        }
368
369
        // Persist the object resource
370 4
        return $this->persistObjectResource($object);
371
    }
372
373
    /**
374
     * Publish an object in the repository
375
     *
376
     * @param ObjectInterface $object
377
     */
378 2
    protected function publishObject(ObjectInterface $object)
379
    {
380 2
        $objectRepositoryPath = $object->getRepositoryPath();
381
382
        // If the object had been persisted as a draft: Remove the draft resource
383 2
        $objectDraftPath = $objectRepositoryPath->setRevision($object->getRevision()->setDraft(true));
384 2
        $absObjectDraftPath = $this->getAbsoluteResourcePath($objectDraftPath);
385 2
        if (@file_exists($absObjectDraftPath)) {
386 2
            unlink($absObjectDraftPath);
387
        }
388
389
        // If it's not the first object revision: Rotate the previous revision resource
390 2
        $objectRevisionNumber = $object->getRevision()->getRevision();
391 2
        if ($objectRevisionNumber > 1) {
392
            // Build the "current" object repository path
393 2
            $currentRevision = Revision::current();
394
            $curObjectResPath =
395 2
                $this->getAbsoluteResourcePath($objectRepositoryPath->setRevision($currentRevision));
396
397
            // Build the previous object repository path
398
            /** @var Revision $previousRevision */
399 2
            $previousRevision = Kernel::create(Revision::class, [$objectRevisionNumber - 1]);
400
            $prevObjectResPath
401 2
                = $this->getAbsoluteResourcePath($objectRepositoryPath->setRevision($previousRevision));
402
403
            // Rotate the previous revision's resource path
404 2
            if (file_exists($curObjectResPath)) {
405 2
                rename($curObjectResPath, $prevObjectResPath);
406
            }
407
        }
408 2
    }
409
410
    /**
411
     * Build an absolute repository resource path
412
     *
413
     * @param RepositoryPathInterface $repositoryPath Repository path
414
     * @return string Absolute repository resource path
415
     */
416 4
    public function getAbsoluteResourcePath(RepositoryPathInterface $repositoryPath)
417
    {
418 4
        return $this->root.str_replace(
419 4
            '/',
420 4
            DIRECTORY_SEPARATOR,
421 4
            $repositoryPath->withExtension(getenv('OBJECT_RESOURCE_EXTENSION'))
422
        );
423
    }
424
425
    /**
426
     * Persist an object resource in the repository
427
     *
428
     * @param ObjectInterface $object Object
429
     * @return AdapterStrategyInterface Self reference
430
     */
431 4
    protected function persistObjectResource(ObjectInterface $object)
432
    {
433
        /** @var \Apparat\Object\Infrastructure\Model\Object\Resource $objectResource */
434 4
        $objectResource = ResourceFactory::createFromObject($object);
435
436
        // Create the absolute object resource path
437 4
        $objectResourcePath = $this->getAbsoluteResourcePath($object->getRepositoryPath());
438
439
        /** @var Writer $fileWriter */
440 4
        $fileWriter = Kernel::create(
441 4
            Writer::class,
442 4
            [$objectResourcePath, Writer::FILE_CREATE | Writer::FILE_CREATE_DIRS | Writer::FILE_OVERWRITE]
443
        );
444 4
        $objectResource->dump($fileWriter);
445
446 4
        return $this;
447
    }
448
449
    /**
450
     * Return the repository size (number of objects in the repository)
451
     *
452
     * @return int Repository size
453
     */
454 5
    public function getRepositorySize()
455
    {
456 5
        $sizeDescriptorFile = $this->configDir.'size.txt';
457 5
        $repositorySize = 0;
458 5
        if (is_file($sizeDescriptorFile) && is_readable($sizeDescriptorFile)) {
459 5
            $repositorySize = intval(file_get_contents($this->configDir.'size.txt'));
460
        }
461 5
        return $repositorySize;
462
    }
463
464
    /**
465
     * Delete all revisions of an object
466
     *
467
     * @param ObjectInterface $object Object
468
     * @return ObjectInterface Object
469
     */
470 3
    protected function deleteObject(ObjectInterface $object)
471
    {
472
        // Hide object directory
473 3
        $objContainerDir = dirname(dirname($this->getAbsoluteResourcePath($object->getRepositoryPath())));
474 3
        $objContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
475 3
        $objPublicContainer = $objContainerDir.DIRECTORY_SEPARATOR.$objContainerName;
476 3
        $objHiddenContainer = $objContainerDir.DIRECTORY_SEPARATOR.'.'.$objContainerName;
477 3
        if (file_exists($objPublicContainer)
478 3
            && is_dir($objPublicContainer)
479 3
            && !rename($objPublicContainer, $objHiddenContainer)
480
        ) {
481 1
            throw new RuntimeException(
482 1
                sprintf('Cannot hide object container "%s"', $objContainerName),
483 1
                RuntimeException::CANNOT_HIDE_OBJECT_CONTAINER
484
            );
485
        }
486
487
        // Delete all object revisions
488
        /** @var ObjectInterface $objectRevision */
489 2
        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...
490 2
            $this->persistObjectResource($objectRevision->delete());
491
        }
492
493 2
        return $this;
494
    }
495
496
    /**
497
     * Undelete all revisions of an object
498
     *
499
     * @param ObjectInterface $object Object
500
     * @return ObjectInterface Object
501
     */
502 2
    protected function undeleteObject(ObjectInterface $object)
503
    {
504
        // Hide object directory
505 2
        $objContainerDir = dirname(dirname($this->getAbsoluteResourcePath($object->getRepositoryPath())));
506 2
        $objContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
507 2
        $objPublicContainer = $objContainerDir.DIRECTORY_SEPARATOR.$objContainerName;
508 2
        $objHiddenContainer = $objContainerDir.DIRECTORY_SEPARATOR.'.'.$objContainerName;
509 2
        if (file_exists($objHiddenContainer)
510 2
            && is_dir($objHiddenContainer)
511 2
            && !rename($objHiddenContainer, $objPublicContainer)
512
        ) {
513 1
            throw new RuntimeException(
514 1
                sprintf('Cannot unhide object container "%s"', $objContainerName),
515 1
                RuntimeException::CANNOT_UNHIDE_OBJECT_CONTAINER
516
            );
517
        }
518
519
        // Undelete all object revisions
520
        /** @var ObjectInterface $objectRevision */
521 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...
522 1
            $this->persistObjectResource($objectRevision->undelete());
523
        }
524
525 1
        return $this;
526
    }
527
}
528