Completed
Push — master ( a721bd...6a60a9 )
by Joschi
03:34
created

FileAdapterStrategy::getAbsoluteResourcePath()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 1

Importance

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