Completed
Push — master ( f0adbf...7c6605 )
by Joschi
03:16 queued 30s
created

FileAdapterStrategy::deleteObject()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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