Completed
Push — master ( b1b8bf...c9de9a )
by Joschi
03:57
created

FileAdapterStrategy::persistObject()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 9
CRAP Score 4

Importance

Changes 4
Bugs 0 Features 2
Metric Value
cc 4
eloc 8
c 4
b 0
f 2
nc 4
nop 1
dl 0
loc 18
rs 9.2
ccs 9
cts 9
cp 1
crap 4
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
    public function hasObjectResource($resourcePath)
250
    {
251
//        echo 'testing '.$this->root.$resourcePath;
252
        return is_file($this->root.$resourcePath);
253
    }
254
255
    /**
256
     * Find and return an object resource
257
     *
258
     * @param string $resourcePath Repository relative resource path
259
     * @return ResourceInterface Object resource
260
     */
261 26
    public function getObjectResource($resourcePath)
262
    {
263 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...
264
    }
265
266
    /**
267
     * Allocate an object ID and create an object resource
268
     *
269
     * @param \Closure $creator Object creation closure
270
     * @return ObjectInterface Object
271
     * @throws RuntimeException If no object could be created
272
     * @throws \Exception If another error occurs
273
     */
274 4
    public function createObjectResource(\Closure $creator)
275
    {
276 4
        $sizeDescriptor = null;
277
278
        try {
279
            // Open the size descriptor
280 4
            $sizeDescriptor = fopen($this->configDir.'size.txt', 'r+');
281
282
            // If a lock of the size descriptor can be acquired
283 4
            if (flock($sizeDescriptor, LOCK_EX)) {
284
                // Determine the current repository size
285 3
                $repositorySize = '';
286 3
                while (!feof($sizeDescriptor)) {
287 3
                    $repositorySize .= fread($sizeDescriptor, 8);
288 3
                }
289 3
                $repositorySize = intval(trim($repositorySize));
290
291
                // Instantiate the next consecutive object ID
292 3
                $nextObjectId = Kernel::create(Id::class, [++$repositorySize]);
293
294
                // Create & persist the object (bypassing the repository)
295 3
                $object = $creator($nextObjectId);
296 3
                $this->persistObject($object);
297
298
                // Dump the new repository size, unlock the size descriptor
299 3
                ftruncate($sizeDescriptor, 0);
300 3
                fwrite($sizeDescriptor, $repositorySize);
301 3
                fflush($sizeDescriptor);
302 3
                flock($sizeDescriptor, LOCK_UN);
303
304
                // Return the newly created object
305 3
                return $object;
306
            }
307
308
            // If no object could be created
309 1
            throw new RuntimeException(
310 1
                'The repository size descriptor is unlockable',
311
                RuntimeException::REPO_SIZE_DESCRIPTOR_UNLOCKABLE
312 1
            );
313
314
            // If any exception is thrown
315 1
        } catch (\Exception $e) {
316
            // Release the size descriptor lock
317 1
            if (is_resource($sizeDescriptor)) {
318 1
                flock($sizeDescriptor, LOCK_UN);
319 1
            }
320
321
            // Forward the thrown exception
322 1
            throw $e;
323
        }
324
    }
325
326
    /**
327
     * Persist an object in the repository
328
     *
329
     * @param ObjectInterface $object Object
330
     * @return AdapterStrategyInterface Self reference
331
     */
332 3
    public function persistObject(ObjectInterface $object)
333
    {
334
        // If the object has just been deleted
335 3
        if ($object->hasBeenDeleted()) {
336 3
            return $this->deleteObject($object);
337
338
            // Elseif the object has just been undeleted
339 3
        } elseif ($object->hasBeenUndeleted()) {
340 2
            return $this->undeleteObject($object);
341
342
            // If the object has just been published
343 3
        } elseif ($object->hasBeenPublished()) {
344 1
            $this->publishObject($object);
345 1
        }
346
347
        // Persist the object resource
348 3
        return $this->persistObjectResource($object);
349
    }
350
351
    /**
352
     * Publish an object in the repository
353
     *
354
     * @param ObjectInterface $object
355
     */
356 1
    protected function publishObject(ObjectInterface $object)
357
    {
358 1
        $objectRepositoryPath = $object->getRepositoryPath();
359
360
        // If the object had been persisted as a draft: Remove the draft resource
361 1
        $objectDraftPath = $objectRepositoryPath->setRevision($object->getRevision()->setDraft(true));
362 1
        $absObjectDraftPath = $this->absoluteResourcePath($objectDraftPath);
363 1
        if (@file_exists($absObjectDraftPath)) {
364 1
            unlink($absObjectDraftPath);
365 1
        }
366
367
        // If it's not the first object revision: Rotate the previous revision resource
368 1
        $objectRevisionNumber = $object->getRevision()->getRevision();
369 1
        if ($objectRevisionNumber > 1) {
370
            // Build the "current" object repository path
371 1
            $currentRevision = Revision::current();
372
            $curObjectResPath =
373 1
                $this->absoluteResourcePath($objectRepositoryPath->setRevision($currentRevision));
374
375
            // Build the previous object repository path
376
            /** @var Revision $previousRevision */
377 1
            $previousRevision = Kernel::create(Revision::class, [$objectRevisionNumber - 1]);
378
            $prevObjectResPath
379 1
                = $this->absoluteResourcePath($objectRepositoryPath->setRevision($previousRevision));
380
381
            // Rotate the previous revision's resource path
382 1
            if (file_exists($curObjectResPath)) {
383 1
                rename($curObjectResPath, $prevObjectResPath);
384 1
            }
385 1
        }
386 1
    }
387
388
    /**
389
     * Build an absolute repository resource path
390
     *
391
     * @param RepositoryPathInterface $repositoryPath Repository path
392
     * @return string Absolute repository resource path
393
     */
394 3
    protected function absoluteResourcePath(RepositoryPathInterface $repositoryPath)
395
    {
396 3
        return $this->root.str_replace(
397 3
            '/',
398 3
            DIRECTORY_SEPARATOR,
399 3
            $repositoryPath->withExtension(getenv('OBJECT_RESOURCE_EXTENSION'))
400 3
        );
401
    }
402
403
    /**
404
     * Persist an object resource in the repository
405
     *
406
     * @param ObjectInterface $object Object
407
     * @return AdapterStrategyInterface Self reference
408
     */
409 3
    protected function persistObjectResource(ObjectInterface $object)
410
    {
411
        /** @var \Apparat\Object\Infrastructure\Model\Object\Resource $objectResource */
412 3
        $objectResource = ResourceFactory::createFromObject($object);
413
414
        // Create the absolute object resource path
415 3
        $objectResourcePath = $this->absoluteResourcePath($object->getRepositoryPath());
416
417
        /** @var Writer $fileWriter */
418 3
        $fileWriter = Kernel::create(
419 3
            Writer::class,
420 3
            [$objectResourcePath, Writer::FILE_CREATE | Writer::FILE_CREATE_DIRS | Writer::FILE_OVERWRITE]
421 3
        );
422 3
        $objectResource->dump($fileWriter);
423
424 3
        return $this;
425
    }
426
427
    /**
428
     * Return the repository size (number of objects in the repository)
429
     *
430
     * @return int Repository size
431
     */
432 4
    public function getRepositorySize()
433
    {
434 4
        $sizeDescriptorFile = $this->configDir.'size.txt';
435 4
        $repositorySize = 0;
436 4
        if (is_file($sizeDescriptorFile) && is_readable($sizeDescriptorFile)) {
437 4
            $repositorySize = intval(file_get_contents($this->configDir.'size.txt'));
438 4
        }
439 4
        return $repositorySize;
440
    }
441
442
    /**
443
     * Delete all revisions of an object
444
     *
445
     * @param ObjectInterface $object Object
446
     * @return ObjectInterface Object
447
     */
448 3
    protected function deleteObject(ObjectInterface $object)
449
    {
450
        // Hide object directory
451 3
        $objectContainerDir = dirname(dirname($this->absoluteResourcePath($object->getRepositoryPath())));
452 3
        $objectContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
453 3
        $objectPublicContainer = $objectContainerDir.DIRECTORY_SEPARATOR.$objectContainerName;
454 3
        $objectHiddenContainer = $objectContainerDir.DIRECTORY_SEPARATOR.'.'.$objectContainerName;
455 3
        if (file_exists($objectPublicContainer)
456 3
            && is_dir($objectPublicContainer)
457 3
            && !rename($objectPublicContainer, $objectHiddenContainer)
458 3
        ) {
459 1
            throw new RepositoryRuntimeException(
460 1
                sprintf('Cannot hide object container "%s"', $objectContainerName),
461
                RepositoryRuntimeException::CANNOT_HIDE_OBJECT_CONTAINER
462 1
            );
463
        }
464
465
        // Delete all object revisions
466
        /** @var ObjectInterface $objectRevision */
467 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...
468 2
            $this->persistObjectResource($objectRevision->delete());
469 2
        }
470
471 2
        return $this;
472
    }
473
474
    /**
475
     * Undelete all revisions of an object
476
     *
477
     * @param ObjectInterface $object Object
478
     * @return ObjectInterface Object
479
     */
480 2
    protected function undeleteObject(ObjectInterface $object)
481
    {
482
        // Hide object directory
483 2
        $objectContainerDir = dirname(dirname($this->absoluteResourcePath($object->getRepositoryPath())));
484 2
        $objectContainerName = $object->getId()->getId().'-'.$object->getType()->getType();
485 2
        $objectPublicContainer = $objectContainerDir.DIRECTORY_SEPARATOR.$objectContainerName;
486 2
        $objectHiddenContainer = $objectContainerDir.DIRECTORY_SEPARATOR.'.'.$objectContainerName;
487 2
        if (file_exists($objectHiddenContainer)
488 2
            && is_dir($objectHiddenContainer)
489 2
            && !rename($objectHiddenContainer, $objectPublicContainer)
490 2
        ) {
491 1
            throw new RepositoryRuntimeException(
492 1
                sprintf('Cannot unhide object container "%s"', $objectContainerName),
493
                RepositoryRuntimeException::CANNOT_UNHIDE_OBJECT_CONTAINER
494 1
            );
495
        }
496
497
        // Undelete all object revisions
498
        /** @var ObjectInterface $objectRevision */
499 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...
500 1
            $this->persistObjectResource($objectRevision->undelete());
501 1
        }
502
503 1
        return $this;
504
    }
505
}
506