Passed
Push — master ( 157732...6d5612 )
by Damian
02:39
created

FilesystemQueue::peek()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

Changes 0
Metric Value
eloc 4
dl 0
loc 9
ccs 5
cts 5
cp 1
rs 10
c 0
b 0
f 0
cc 2
nc 2
nop 0
crap 2
1
<?php declare(strict_types=1);
2
3
namespace Initx\Driver;
4
5
use Initx\Envelope;
6
use Initx\Exception\IllegalStateException;
7
use Initx\Exception\NoSuchElementException;
8
use Initx\Queue;
9
use JMS\Serializer\SerializerBuilder;
10
use JMS\Serializer\SerializerInterface;
11
use Throwable;
12
13
final class FilesystemQueue implements Queue
14
{
15
    /**
16
     * @var string
17
     */
18
    private $path;
19
20
    /**
21
     * @var SerializerInterface
22
     */
23
    private $serializer;
24
25 11
    public function __construct(string $path, SerializerInterface $serializer = null)
26
    {
27 11
        if (!$serializer) {
28 11
            $separator = DIRECTORY_SEPARATOR;
29 11
            $metaDir = sprintf('%s%s..%s..%sconfig%sjms', __DIR__, $separator, $separator, $separator, $separator);
30 11
            $serializer = SerializerBuilder::create()
31 11
                ->addMetadataDir($metaDir, 'Initx')
32 11
                ->build();
33
        }
34 11
        $this->serializer = $serializer;
35 11
        $this->path = $path;
36 11
    }
37
38
    /**
39
     * Inserts an element if possible, otherwise throwing exception.
40
     *
41
     * @param Envelope $envelope
42
     * @return void
43
     * @throws IllegalStateException
44
     */
45 5
    public function add(Envelope $envelope): void
46
    {
47 5
        if (!$this->write($envelope)) {
48 1
            throw IllegalStateException::create("Could not write to {$this->path}");
49
        }
50 4
    }
51
52
    /**
53
     * Inserts an element if possible.
54
     *
55
     * @param Envelope $envelope
56
     * @return void
57
     */
58 2
    public function offer(Envelope $envelope): void
59
    {
60 2
        $this->write($envelope);
61 2
    }
62
63 7
    private function write(Envelope $envelope): bool
64
    {
65 7
        $content = $this->serializer->serialize($envelope, 'json') . PHP_EOL;
66
        try {
67 7
            $result = (bool)file_put_contents(
68 7
                $this->path,
69 7
                $content,
70 7
                FILE_APPEND
71
            );
72 2
        } catch (Throwable $exception) {
73 2
            $result = false;
74
        }
75
76 7
        return $result;
77
    }
78
79
    /**
80
     * Remove and return head of queue, otherwise throwing exception.
81
     *
82
     * @return Envelope
83
     * @throws NoSuchElementException | IllegalStateException
84
     */
85 3
    public function remove(): Envelope
86
    {
87 3
        if (!$envelope = $this->poll()) {
88 1
            throw new NoSuchElementException('Queue empty');
89
        }
90
91 1
        return $envelope;
92
    }
93
94 3
    private function removeFirstLine(): ?string
95
    {
96 3
        if (!file_exists($this->path)) {
97 1
            throw new IllegalStateException("File $this->path not exists");
98
        }
99 2
        $firstLine = null;
100 2
        if ($handle = fopen($this->path, 'cb+')) {
101 2
            if (!flock($handle, LOCK_EX)) {
102
                fclose($handle);
103
            }
104 2
            $offset = 0;
105 2
            $len = filesize($this->path);
106 2
            while (($line = fgets($handle, 4096)) !== false) {
107 1
                if (!$firstLine) {
108 1
                    $firstLine = $line;
109 1
                    $offset = strlen($firstLine);
110 1
                    continue;
111
                }
112 1
                $pos = ftell($handle);
113 1
                fseek($handle, $pos - strlen($line) - $offset);
114 1
                fwrite($handle, $line);
115 1
                fseek($handle, $pos);
116
            }
117 2
            fflush($handle);
118 2
            ftruncate($handle, $len - $offset);
119 2
            flock($handle, LOCK_UN);
120 2
            fclose($handle);
121
        }
122
123 2
        return $firstLine;
124
    }
125
126 4
    private function readFirstLine(): ?string
127
    {
128 4
        if (!file_exists($this->path)) {
129
            throw new IllegalStateException("File $this->path not exists");
130
        }
131
132 4
        $firstLine = fgets(fopen($this->path, 'rb'));
0 ignored issues
show
Bug introduced by
It seems like fopen($this->path, 'rb') can also be of type false; however, parameter $handle of fgets() does only seem to accept resource, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

132
        $firstLine = fgets(/** @scrutinizer ignore-type */ fopen($this->path, 'rb'));
Loading history...
133
134 4
        return $firstLine ?: null;
135
    }
136
137
    /**
138
     * Remove and return head of queue, otherwise returning null.
139
     *
140
     * @return Envelope | null
141
     * @throws IllegalStateException
142
     */
143 3
    public function poll(): ?Envelope
144
    {
145 3
        $firstLine = $this->removeFirstLine();
146
147 2
        if (!$firstLine) {
148 1
            return null;
149
        }
150
151 1
        return $this->serializer->deserialize($firstLine, Envelope::class, 'json');
152
    }
153
154
    /**
155
     * Return but do not remove head of queue, otherwise throwing exception.
156
     *
157
     * @return Envelope
158
     * @throws NoSuchElementException | IllegalStateException
159
     */
160 2
    public function element(): Envelope
161
    {
162 2
        if (!$envelope = $this->peek()) {
163 1
            throw new NoSuchElementException('Queue empty');
164
        }
165
166 1
        return $envelope;
167
    }
168
169
    /**
170
     * Return but do not remove head of queue, otherwise returning null.
171
     *
172
     * @return Envelope | null
173
     * @throws IllegalStateException
174
     */
175 4
    public function peek(): ?Envelope
176
    {
177 4
        $firstLine = $this->readFirstLine();
178
179 4
        if (!$firstLine) {
180 2
            return null;
181
        }
182
183 2
        return $this->serializer->deserialize($firstLine, Envelope::class, 'json');
184
    }
185
}
186