Passed
Push — master ( 98c079...34bb2f )
by Fabrice
01:57
created

FileLock::obtainLockHandleFallBack()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
c 0
b 0
f 0
nc 2
nop 0
dl 0
loc 12
rs 10
1
<?php
2
3
/*
4
 * This file is part of FileLock.
5
 *     (c) Fabrice de Stefanis / https://github.com/fab2s/OpinHelpers
6
 * This source file is licensed under the MIT license which you will
7
 * find in the LICENSE file or at https://opensource.org/licenses/MIT
8
 */
9
10
namespace fab2s\FileLock;
11
12
/**
13
 * Class FileLock
14
 */
15
class FileLock
16
{
17
    /**
18
     * external lock type actually locks "inputFile.lock"
19
     */
20
    const LOCK_EXTERNAL = 'external';
21
22
    /**
23
     * lock the file itself
24
     */
25
    const LOCK_SELF = 'self';
26
27
    /**
28
     * @var string
29
     */
30
    protected $lockType = self::LOCK_EXTERNAL;
31
32
    /**
33
     * max number of lock attempt
34
     *
35
     * @var int
36
     */
37
    protected $lockTry = 3;
38
39
    /**
40
     * The number of seconds to wait between lock attempts
41
     *
42
     * @var float
43
     */
44
    protected $lockWait = 0.1;
45
46
    /**
47
     * @var string
48
     */
49
    protected $file;
50
51
    /**
52
     * @var resource
53
     */
54
    protected $handle;
55
56
    /**
57
     * @var string fopen mode
58
     */
59
    protected $mode;
60
61
    /**
62
     * @var bool
63
     */
64
    protected $lockAcquired = false;
65
66
    /**
67
     * FileLock constructor.
68
     *
69
     * @param string $file
70
     * @param string $lockMethod
71
     * @param string $mode
72
     *
73
     * @throws \InvalidArgumentException
74
     */
75
    public function __construct(string $file, string $lockMethod, string $mode = 'wb')
76
    {
77
        $fileDir = dirname($file);
78
        if (!($fileDir = realpath($fileDir))) {
79
            throw new \InvalidArgumentException('File path not valid');
80
        }
81
82
        if ($lockMethod === self::LOCK_SELF) {
83
            $this->lockType = self::LOCK_SELF;
84
            $this->mode     = $mode;
85
            $this->file     = $fileDir . '/' . basename($file);
86
87
            return;
88
        }
89
90
        $fileDir    = is_writeable($fileDir) ? $fileDir . '/' : sys_get_temp_dir() . '/' . sha1($fileDir) . '_';
91
        $this->file = $fileDir . basename($file) . '.lock';
92
    }
93
94
    /**
95
     * since there is no more auto unlocking
96
     */
97
    public function __destruct()
98
    {
99
        $this->unLock();
100
    }
101
102
    /**
103
     * @param string         $file
104
     * @param string         $mode     fopen() mode
105
     * @param int|null       $maxTries 0|null for single non blocking attempt
106
     *                                 1 for a single blocking attempt
107
     *                                 1-N Number of non blocking attempts
108
     * @param float|int|null $lockWait Time to wait between attempts in second
109
     *
110
     * @return null|static
111
     */
112
    public static function open(string $file, string $mode, ?int $maxTries = null, $lockWait = null): ? self
113
    {
114
        $instance = new static($file, self::LOCK_SELF, $mode);
115
        $maxTries = max(0, (int) $maxTries);
116
        if ($maxTries > 1) {
117
            $instance->setLockTry($maxTries);
118
            $lockWait = max(0, (float) $lockWait);
119
            if ($lockWait > 0) {
120
                $instance->setLockWait($lockWait);
121
            }
122
            $instance->obtainLock();
123
        } else {
124
            $instance->doLock((bool) $maxTries);
125
        }
126
127
        if ($instance->isLocked()) {
128
            return $instance;
129
        }
130
131
        $instance->unLock();
132
133
        return null;
134
    }
135
136
    /**
137
     * @return resource
138
     */
139
    public function getHandle()
140
    {
141
        return $this->handle;
142
    }
143
144
    /**
145
     * @return string
146
     */
147
    public function getLockType(): string
148
    {
149
        return $this->lockType;
150
    }
151
152
    /**
153
     * @return bool
154
     */
155
    public function isLocked(): bool
156
    {
157
        return $this->lockAcquired;
158
    }
159
160
    /**
161
     * obtain a lock with retries
162
     *
163
     * @return bool
164
     */
165
    public function obtainLock(): bool
166
    {
167
        $tries       = 0;
168
        $waitClosure = $this->getWaitClosure();
169
        do {
170
            if ($this->doLock()) {
171
                return true;
172
            }
173
174
            ++$tries;
175
            $waitClosure();
176
        } while ($tries < $this->lockTry);
177
178
        return false;
179
    }
180
181
    /**
182
     * @param bool $blocking
183
     *
184
     * @return bool
185
     */
186
    public function doLock(bool $blocking = false): bool
187
    {
188
        if ($this->lockAcquired) {
189
            return true;
190
        }
191
192
        if ($this->obtainLockHandle()) {
193
            $this->lockAcquired = flock($this->handle, $blocking ? LOCK_EX : LOCK_EX | LOCK_NB);
194
        }
195
196
        if (!$this->lockAcquired) {
197
            $this->unLock();
198
        }
199
200
        return $this->lockAcquired;
201
    }
202
203
    /**
204
     * release the lock
205
     *
206
     * @return static
207
     */
208
    public function unLock(): self
209
    {
210
        if (is_resource($this->handle)) {
211
            fflush($this->handle);
212
            flock($this->handle, LOCK_UN);
213
            fclose($this->handle);
214
        }
215
216
        $this->lockAcquired = false;
217
        $this->handle       = null;
218
219
        return $this;
220
    }
221
222
    /**
223
     * @param int $number
224
     *
225
     * @return static
226
     */
227
    public function setLockTry(int $number): self
228
    {
229
        $this->lockTry = max(1, (int) $number);
230
231
        return $this;
232
    }
233
234
    /**
235
     * @param float|int $seconds
236
     *
237
     * @return static
238
     */
239
    public function setLockWait($seconds)
240
    {
241
        $this->lockWait = max(0.0001, (float) $seconds);
242
243
        return $this;
244
    }
245
246
    /**
247
     * @return bool
248
     */
249
    protected function obtainLockHandle(): bool
250
    {
251
        $this->mode   = $this->mode ?: (is_file($this->file) ? 'rb' : 'wb');
252
        $this->handle = fopen($this->file, $this->mode) ?: null;
253
        if (!$this->handle) {
254
            return $this->obtainLockHandleFallBack();
255
        }
256
257
        return true;
258
    }
259
260
    /**
261
     * @return bool
262
     */
263
    protected function obtainLockHandleFallBack(): bool
264
    {
265
        if (
266
            $this->lockType === self::LOCK_EXTERNAL &&
267
            $this->mode === 'wb'
268
        ) {
269
            // if another process won the race at creating lock file
270
            $this->mode   = 'rb';
271
            $this->handle = fopen($this->file, $this->mode) ?: null;
272
        }
273
274
        return (bool) $this->handle;
275
    }
276
277
    /**
278
     * @return \Closure
279
     */
280
    protected function getWaitClosure(): \Closure
281
    {
282
        if ($this->lockWait > 300) {
283
            $wait = (int) $this->lockWait;
284
285
            return function () use ($wait) {
286
                sleep($wait);
287
            };
288
        }
289
290
        $wait = (int) ($this->lockWait * 1000000);
291
292
        return function () use ($wait) {
293
            usleep($wait);
294
        };
295
    }
296
}
297