Completed
Push — master ( 42ada5...e5d943 )
by Joschi
02:42
created

FileAdapterStrategy::persistObject()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 20
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 20
ccs 10
cts 10
cp 1
rs 9.4285
cc 1
eloc 9
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\Path\PathInterface;
45
use Apparat\Object\Domain\Model\Path\RepositoryPath;
46
use Apparat\Object\Domain\Repository\RepositoryInterface;
47
use Apparat\Object\Domain\Repository\RuntimeException;
48
use Apparat\Object\Domain\Repository\Selector;
49
use Apparat\Object\Domain\Repository\SelectorInterface;
50
use Apparat\Object\Infrastructure\Factory\ResourceFactory;
51
use Apparat\Resource\Infrastructure\Io\File\AbstractFileReaderWriter;
52
use Apparat\Resource\Infrastructure\Io\File\Writer;
53
54
/**
55
 * File adapter strategy
56
 *
57
 * @package Apparat\Object
58
 * @subpackage Apparat\Object\Infrastructure
59
 */
60
class FileAdapterStrategy extends AbstractAdapterStrategy
61
{
62
    /**
63
     * Adapter strategy type
64
     *
65
     * @var string
66
     */
67
    const TYPE = 'file';
68
    /**
69
     * Configuration
70
     *
71
     * @var array
72
     */
73
    protected $config = null;
74
    /**
75
     * Root directory (without trailing directory separator)
76
     *
77
     * @var string
78
     */
79
    protected $root = null;
80
    /**
81
     * Configuration directory (including trailing directory separator)
82
     *
83
     * @var string
84
     */
85
    protected $configDir = null;
86
87
    /**
88
     * Adapter strategy constructor
89
     *
90
     * @param array $config Adapter strategy configuration
91
     * @throws InvalidArgumentException If the root directory configuration is empty
92
     * @throws InvalidArgumentException If the root directory configuration is invalid
93
     */
94 17
    public function __construct(array $config)
95
    {
96 17
        parent::__construct($config, ['root']);
97
98
        // If the root directory configuration is empty
99 16
        if (empty($this->config['root'])) {
100 1
            throw new InvalidArgumentException(
101 1
                'Empty file adapter strategy root',
102
                InvalidArgumentException::EMTPY_FILE_STRATEGY_ROOT
103 1
            );
104
        }
105
106
        // Get the real path of the root directory
107 15
        $this->root = realpath($this->config['root']);
108
109
        // If the repository should be initialized
110 15
        if (!empty($this->config['init'])
111 15
            && (boolean)$this->config['init']
112 15
            && $this->initializeRepository()
113 13
        ) {
114 3
            $this->root = realpath($this->config['root']);
115 3
        }
116
117
        // If the root directory configuration is still invalid
118 13
        if (empty($this->root) || !@is_dir($this->root)) {
119 1
            throw new InvalidArgumentException(
120 1
                sprintf(
121 1
                    'Invalid file adapter strategy root "%s"',
122 1
                    $this->config['root']
123 1
                ),
124
                InvalidArgumentException::INVALID_FILE_STRATEGY_ROOT
125 1
            );
126
        }
127
128 12
        $this->configDir = $this->root.DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
129 12
    }
130
131
    /**
132
     * Initialize the repository
133
     *
134
     * @return boolean Success
135
     * @throws RuntimeException If the repository cannot be initialized
136
     * @throws RuntimeException If the repository size descriptor can not be created
137
     */
138 5
    public function initializeRepository()
139
    {
140
        // Successively create the repository directories
141 5
        $repoDirectories = [$this->config['root'], $this->config['root'].DIRECTORY_SEPARATOR.'.repo'];
142 5
        foreach ($repoDirectories as $repoDirectory) {
143
            // If the repository cannot be initialized
144 5
            if (file_exists($repoDirectory) ? !is_dir($repoDirectory) : !mkdir($repoDirectory, 0777, true)) {
145 1
                throw new RuntimeException('Could not initialize repository', RuntimeException::REPO_NOT_INITIALIZED);
146
            }
147 4
        }
148
149
        // If the repository size descriptor can not be created
150 4
        $configDir = $this->config['root'].DIRECTORY_SEPARATOR.'.repo'.DIRECTORY_SEPARATOR;
151
        if (
152 4
            (file_exists($configDir.'size.txt') && !is_file($configDir.'size.txt'))
153 3
            || !file_put_contents($configDir.'size.txt', '0')
154 4
        ) {
155 1
            throw new RuntimeException(
156 1
                'Could not create repository size descriptor',
157
                RuntimeException::REPO_SIZE_DESCRIPTOR_NOT_CREATED
158 1
            );
159
        }
160
161 3
        return true;
162
    }
163
164
    /**
165
     * Find objects by selector
166
     *
167
     * @param Selector|SelectorInterface $selector Object selector
168
     * @param RepositoryInterface $repository Object repository
169
     * @return PathInterface[] Object paths
170
     */
171 7
    public function findObjectPaths(SelectorInterface $selector, RepositoryInterface $repository)
172
    {
173 7
        chdir($this->root);
174
175
        // Build a glob string from the selector
176 7
        $glob = '';
177 7
        $globFlags = GLOB_ONLYDIR | GLOB_NOSORT;
178
179 7
        $year = $selector->getYear();
180 7
        if ($year !== null) {
181 7
            $glob .= '/'.$year;
182 7
        }
183
184 7
        $month = $selector->getMonth();
185 7
        if ($month !== null) {
186 7
            $glob .= '/'.$month;
187 7
        }
188
189 7
        $day = $selector->getDay();
190 7
        if ($day !== null) {
191 7
            $glob .= '/'.$day;
192 7
        }
193
194 7
        $hour = $selector->getHour();
195 7
        if ($hour !== null) {
196 2
            $glob .= '/'.$hour;
197 2
        }
198
199 7
        $minute = $selector->getMinute();
200 7
        if ($minute !== null) {
201 2
            $glob .= '/'.$minute;
202 2
        }
203
204 7
        $second = $selector->getSecond();
205 7
        if ($second !== null) {
206 2
            $glob .= '/'.$second;
207 2
        }
208
209 7
        $uid = $selector->getId();
210 7
        $type = $selector->getType();
211 7
        if (($uid !== null) || ($type !== null)) {
212 7
            $glob .= '/'.($uid ?: Selector::WILDCARD).'.'.($type ?: Selector::WILDCARD);
213
214 7
            $revision = $selector->getRevision();
215 7
            if ($revision !== null) {
216 1
                $glob .= '/'.($uid ?: Selector::WILDCARD).'-'.$revision;
217 1
                $globFlags &= ~GLOB_ONLYDIR;
218 1
            }
219 7
        }
220
221 7
        return array_map(
222 7
            function ($objectPath) use ($repository) {
223 7
                return Kernel::create(RepositoryPath::class, [$repository, '/'.$objectPath]);
224 7
            },
225 7
            glob(ltrim($glob, '/'), $globFlags)
226 7
        );
227
    }
228
229
    /**
230
     * Find and return an object resource
231
     *
232
     * @param string $resourcePath Repository relative resource path
233
     * @return ResourceInterface Object resource
234
     */
235 17
    public function getObjectResource($resourcePath)
236
    {
237 17
        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...
238
    }
239
240
    /**
241
     * Allocate an object ID and create an object resource
242
     *
243
     * @param \Closure $creator Object creation closure
244
     * @return ObjectInterface Object
245
     * @throws RuntimeException If no object could be created
246
     * @throws \Exception If another error occurs
247
     */
248 2
    public function createObjectResource(\Closure $creator)
249
    {
250 2
        $sizeDescriptor = null;
251
252
        try {
253
            // Open the size descriptor
254 2
            $sizeDescriptor = fopen($this->configDir.'size.txt', 'r+');
255
256
            // If a lock of the size descriptor can be acquired
257 2
            if (flock($sizeDescriptor, LOCK_EX)) {
258
                // Determine the current repository size
259 1
                $repositorySize = '';
260 1
                while (!feof($sizeDescriptor)) {
261 1
                    $repositorySize .= fread($sizeDescriptor, 8);
262 1
                }
263 1
                $repositorySize = intval(trim($repositorySize));
264
265
                // Instantiate the next consecutive object ID
266 1
                $nextObjectId = Kernel::create(Id::class, [++$repositorySize]);
267
268
                // Create & persist the object
269 1
                $object = $this->persistObject($creator($nextObjectId));
270
271
                // Dump the new repository size, unlock the size descriptor
272 1
                ftruncate($sizeDescriptor, 0);
273 1
                fwrite($sizeDescriptor, $repositorySize);
274 1
                fflush($sizeDescriptor);
275 1
                flock($sizeDescriptor, LOCK_UN);
276
277
                // Return the newly created object
278 1
                return $object;
279
            }
280
281
            // If no object could be created
282 1
            throw new RuntimeException(
283 1
                'The repository size descriptor is unlockable',
284
                RuntimeException::REPO_SIZE_DESCRIPTOR_UNLOCKABLE
285 1
            );
286
287
            // If any exception is thrown
288 1
        } catch (\Exception $e) {
289
            // Release the size descriptor lock
290 1
            if (is_resource($sizeDescriptor)) {
291 1
                flock($sizeDescriptor, LOCK_UN);
292 1
            }
293
294
            // Forward the thrown exception
295 1
            throw $e;
296
        }
297
    }
298
299
    /**
300
     * Persist an object in the repository
301
     *
302
     * @param ObjectInterface $object Object
303
     * @return ObjectInterface Persisted object
304
     */
305 1
    public function persistObject(ObjectInterface $object)
306
    {
307
        /** @var \Apparat\Object\Infrastructure\Model\Object\Resource $objectResource */
308 1
        $objectResource = ResourceFactory::createFromObject($object);
309
310
        // Create the absolute object resource path
311 1
        $objectResourcePath = $this->root.str_replace('/', DIRECTORY_SEPARATOR,
312 1
                $object->getRepositoryPath()->withExtension(getenv('OBJECT_RESOURCE_EXTENSION')));
313
314
        /** @var Writer $fileWriter */
315 1
        $fileWriter = Kernel::create(
316 1
            Writer::class,
317 1
            [$objectResourcePath, Writer::FILE_CREATE | Writer::FILE_CREATE_DIRS | Writer::FILE_OVERWRITE]
318 1
        );
319 1
        $objectResource->dump($fileWriter);
320
321
        // TODO: Set object clean
322
323 1
        return $object;
324
    }
325
326
    /**
327
     * Return the repository size (number of objects in the repository)
328
     *
329
     * @return int Repository size
330
     */
331 2
    public function getRepositorySize()
332
    {
333 2
        $sizeDescriptorFile = $this->configDir.'size.txt';
334 2
        $repositorySize = 0;
335 2
        if (is_file($sizeDescriptorFile) && is_readable($sizeDescriptorFile)) {
336 2
            $repositorySize = intval(file_get_contents($this->configDir.'size.txt'));
337 2
        }
338 2
        return $repositorySize;
339
    }
340
}
341