Completed
Push — master ( c9de9a...8fa92c )
by Joschi
06:37
created

FileAdapterStrategy::getObjectResource()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Apparat\Object\Infrastru...sitory\RuntimeException.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
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\Repository\RuntimeException as RepositoryRuntimeException;
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 19
    public function __construct(array $config)
109
    {
110 19
        parent::__construct($config, ['root']);
111
112
        // If the root directory configuration is empty
113 18
        if (empty($this->config['root'])) {
114 1
            throw new InvalidArgumentException(
115 1
                'Empty file adapter strategy root',
116
                InvalidArgumentException::EMTPY_FILE_STRATEGY_ROOT
117 1
            );
118
        }
119
120
        // Get the real path of the root directory
121 17
        $this->root = realpath($this->config['root']);
122
123
        // If the repository should be initialized
124 17
        if (!empty($this->config['init'])
125 17
            && (boolean)$this->config['init']
126 17
            && $this->initializeRepository()
127 15
        ) {
128 5
            $this->root = realpath($this->config['root']);
129 5
        }
130
131
        // If the root directory configuration is still invalid
132 15
        if (empty($this->root) || !@is_dir($this->root)) {
133 1
            throw new InvalidArgumentException(
134 1
                sprintf(
135 1
                    'Invalid file adapter strategy root "%s"',
136 1
                    $this->config['root']
137 1
                ),
138
                InvalidArgumentException::INVALID_FILE_STRATEGY_ROOT
139 1
            );
140
        }
141
142 14
        $this->configDir = $this->root.DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
143 14
    }
144
145
    /**
146
     * Initialize the repository
147
     *
148
     * @return boolean Success
149
     * @throws RuntimeException If the repository cannot be initialized
150
     * @throws RuntimeException If the repository size descriptor can not be created
151
     */
152 7
    public function initializeRepository()
153
    {
154
        // Successively create the repository directories
155 7
        $repoDirectories = [$this->config['root'], $this->config['root'].DIRECTORY_SEPARATOR.'.repo'];
156 7
        foreach ($repoDirectories as $repoDirectory) {
157
            // If the repository cannot be initialized
158 7
            if (file_exists($repoDirectory) ? !is_dir($repoDirectory) : !mkdir($repoDirectory, 0777, true)) {
159 1
                throw new RuntimeException('Could not initialize repository', RuntimeException::REPO_NOT_INITIALIZED);
160
            }
161 6
        }
162
163
        // If the repository size descriptor can not be created
164 6
        $configDir = $this->config['root'].DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
165 6
        if ((file_exists($configDir.'size.txt') && !is_file($configDir.'size.txt'))
166 5
            || !file_put_contents($configDir.'size.txt', '0')
167 6
        ) {
168 1
            throw new RuntimeException(
169 1
                'Could not create repository size descriptor',
170
                RuntimeException::REPO_SIZE_DESCRIPTOR_NOT_CREATED
171 1
            );
172
        }
173
174 5
        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 7
        }
196
197 7
        $month = $selector->getMonth();
198 7
        if ($month !== null) {
199 7
            $glob .= '/'.$month;
200 7
        }
201
202 7
        $day = $selector->getDay();
203 7
        if ($day !== null) {
204 7
            $glob .= '/'.$day;
205 7
        }
206
207 7
        $hour = $selector->getHour();
208 7
        if ($hour !== null) {
209 2
            $glob .= '/'.$hour;
210 2
        }
211
212 7
        $minute = $selector->getMinute();
213 7
        if ($minute !== null) {
214 2
            $glob .= '/'.$minute;
215 2
        }
216
217 7
        $second = $selector->getSecond();
218 7
        if ($second !== null) {
219 2
            $glob .= '/'.$second;
220 2
        }
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 1
            }
233 7
        }
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 7
        );
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 27
    public function hasObjectResource($resourcePath)
250
    {
251 27
        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 26
    public function getObjectResource($resourcePath)
261
    {
262 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...
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 4
    public function createObjectResource(\Closure $creator)
274
    {
275 4
        $sizeDescriptor = null;
276
277
        try {
278
            // Open the size descriptor
279 4
            $sizeDescriptor = fopen($this->configDir.'size.txt', 'r+');
280
281
            // If a lock of the size descriptor can be acquired
282 4
            if (flock($sizeDescriptor, LOCK_EX)) {
283
                // Determine the current repository size
284 3
                $repositorySize = '';
285 3
                while (!feof($sizeDescriptor)) {
286 3
                    $repositorySize .= fread($sizeDescriptor, 8);
287 3
                }
288 3
                $repositorySize = intval(trim($repositorySize));
289
290
                // Instantiate the next consecutive object ID
291 3
                $nextObjectId = Kernel::create(Id::class, [++$repositorySize]);
292
293
                // Create & persist the object (bypassing the repository)
294 3
                $object = $creator($nextObjectId);
295 3
                $this->persistObject($object);
296
297
                // Dump the new repository size, unlock the size descriptor
298 3
                ftruncate($sizeDescriptor, 0);
299 3
                fwrite($sizeDescriptor, $repositorySize);
300 3
                fflush($sizeDescriptor);
301 3
                flock($sizeDescriptor, LOCK_UN);
302
303
                // Return the newly created object
304 3
                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 3
    public function persistObject(ObjectInterface $object)
332
    {
333
        // If the object has just been deleted
334 3
        if ($object->hasBeenDeleted()) {
335 3
            return $this->deleteObject($object);
336
337
            // Elseif the object has just been undeleted
338 3
        } elseif ($object->hasBeenUndeleted()) {
339 2
            return $this->undeleteObject($object);
340
341
            // If the object has just been published
342 3
        } elseif ($object->hasBeenPublished()) {
343 1
            $this->publishObject($object);
344 1
        }
345
346
        // Persist the object resource
347 3
        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 3
    protected function absoluteResourcePath(RepositoryPathInterface $repositoryPath)
394
    {
395 3
        return $this->root.str_replace(
396 3
            '/',
397 3
            DIRECTORY_SEPARATOR,
398 3
            $repositoryPath->withExtension(getenv('OBJECT_RESOURCE_EXTENSION'))
399 3
        );
400
    }
401
402
    /**
403
     * Persist an object resource in the repository
404
     *
405
     * @param ObjectInterface $object Object
406
     * @return AdapterStrategyInterface Self reference
407
     */
408 3
    protected function persistObjectResource(ObjectInterface $object)
409
    {
410
        /** @var \Apparat\Object\Infrastructure\Model\Object\Resource $objectResource */
411 3
        $objectResource = ResourceFactory::createFromObject($object);
412
413
        // Create the absolute object resource path
414 3
        $objectResourcePath = $this->absoluteResourcePath($object->getRepositoryPath());
415
416
        /** @var Writer $fileWriter */
417 3
        $fileWriter = Kernel::create(
418 3
            Writer::class,
419 3
            [$objectResourcePath, Writer::FILE_CREATE | Writer::FILE_CREATE_DIRS | Writer::FILE_OVERWRITE]
420 3
        );
421 3
        $objectResource->dump($fileWriter);
422
423 3
        return $this;
424
    }
425
426
    /**
427
     * Return the repository size (number of objects in the repository)
428
     *
429
     * @return int Repository size
430
     */
431 4
    public function getRepositorySize()
432
    {
433 4
        $sizeDescriptorFile = $this->configDir.'size.txt';
434 4
        $repositorySize = 0;
435 4
        if (is_file($sizeDescriptorFile) && is_readable($sizeDescriptorFile)) {
436 4
            $repositorySize = intval(file_get_contents($this->configDir.'size.txt'));
437 4
        }
438 4
        return $repositorySize;
439
    }
440
441
    /**
442
     * Delete all revisions of an object
443
     *
444
     * @param ObjectInterface $object Object
445
     * @return ObjectInterface Object
446
     */
447 3
    protected function deleteObject(ObjectInterface $object)
448
    {
449
        // Hide object directory
450 3
        $objectContainerDir = dirname(dirname($this->absoluteResourcePath($object->getRepositoryPath())));
451 3
        $objectContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
452 3
        $objectPublicContainer = $objectContainerDir.DIRECTORY_SEPARATOR.$objectContainerName;
453 3
        $objectHiddenContainer = $objectContainerDir.DIRECTORY_SEPARATOR.'.'.$objectContainerName;
454 3
        if (file_exists($objectPublicContainer)
455 3
            && is_dir($objectPublicContainer)
456 3
            && !rename($objectPublicContainer, $objectHiddenContainer)
457 3
        ) {
458 1
            throw new RepositoryRuntimeException(
459 1
                sprintf('Cannot hide object container "%s"', $objectContainerName),
460
                RepositoryRuntimeException::CANNOT_HIDE_OBJECT_CONTAINER
461 1
            );
462
        }
463
464
        // Delete all object revisions
465
        /** @var ObjectInterface $objectRevision */
466 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...
467 2
            $this->persistObjectResource($objectRevision->delete());
468 2
        }
469
470 2
        return $this;
471
    }
472
473
    /**
474
     * Undelete all revisions of an object
475
     *
476
     * @param ObjectInterface $object Object
477
     * @return ObjectInterface Object
478
     */
479 2
    protected function undeleteObject(ObjectInterface $object)
480
    {
481
        // Hide object directory
482 2
        $objectContainerDir = dirname(dirname($this->absoluteResourcePath($object->getRepositoryPath())));
483 2
        $objectContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
484 2
        $objectPublicContainer = $objectContainerDir.DIRECTORY_SEPARATOR.$objectContainerName;
485 2
        $objectHiddenContainer = $objectContainerDir.DIRECTORY_SEPARATOR.'.'.$objectContainerName;
486 2
        if (file_exists($objectHiddenContainer)
487 2
            && is_dir($objectHiddenContainer)
488 2
            && !rename($objectHiddenContainer, $objectPublicContainer)
489 2
        ) {
490 1
            throw new RepositoryRuntimeException(
491 1
                sprintf('Cannot unhide object container "%s"', $objectContainerName),
492
                RepositoryRuntimeException::CANNOT_UNHIDE_OBJECT_CONTAINER
493 1
            );
494
        }
495
496
        // Undelete all object revisions
497
        /** @var ObjectInterface $objectRevision */
498 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...
499 1
            $this->persistObjectResource($objectRevision->undelete());
500 1
        }
501
502 1
        return $this;
503
    }
504
}
505