Passed
Push — master ( f87ce8...27b6c5 )
by Fabrice
02:39 queued 26s
created

FileLock::getWaitClosure()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 7
nc 2
nop 0
dl 0
loc 14
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
/*
4
 * This file is part of OpinHelpers.
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\OpinHelpers;
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($file, $lockMethod, $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|null $lockWait Time to wait between attempts in second
109
     *
110
     * @return bool|static
111
     */
112
    public static function open($file, $mode, $maxTries = null, $lockWait = null)
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 false;
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()
148
    {
149
        return $this->lockType;
150
    }
151
152
    /**
153
     * @return bool
154
     */
155
    public function isLocked()
156
    {
157
        return $this->lockAcquired;
158
    }
159
160
    /**
161
     * obtain a lock with retries
162
     *
163
     * @return $this
164
     */
165
    public function obtainLock()
166
    {
167
        $tries       = 0;
168
        $waitClosure = $this->getWaitClosure();
169
        do {
170
            if ($this->doLock()->isLocked()) {
171
                return $this;
172
            }
173
174
            ++$tries;
175
            $waitClosure();
176
        } while ($tries < $this->lockTry);
177
178
        return $this;
179
    }
180
181
    /**
182
     * @param bool $blocking
183
     *
184
     * @return $this
185
     */
186
    public function doLock($blocking = false)
187
    {
188
        if ($this->lockAcquired) {
189
            return $this;
190
        }
191
192
        $this->mode   = $this->mode ?: (is_file($this->file) ? 'rb' : 'wb');
193
        $this->handle = fopen($this->file, $this->mode) ?: null;
194
        if (
195
            $this->lockType === self::LOCK_EXTERNAL &&
196
            $this->mode === 'wb' &&
197
            !$this->handle
198
        ) {
199
            // if another process won the race at creating lock file
200
            $this->mode   = 'rb';
201
            $this->handle = fopen($this->file, $this->mode) ?: null;
202
        }
203
204
        $this->lockAcquired = $this->handle ? flock($this->handle, $blocking ? LOCK_EX : LOCK_EX | LOCK_NB) : false;
205
206
        if (!$this->lockAcquired) {
207
            $this->unLock();
208
        }
209
210
        return $this;
211
    }
212
213
    /**
214
     * release the lock
215
     */
216
    public function unLock()
217
    {
218
        if (is_resource($this->handle)) {
219
            fflush($this->handle);
220
            flock($this->handle, LOCK_UN);
221
            fclose($this->handle);
222
        }
223
224
        $this->lockAcquired = false;
225
        $this->handle       = null;
226
227
        return $this;
228
    }
229
230
    /**
231
     * @param int $number
232
     *
233
     * @return $this
234
     */
235
    public function setLockTry($number)
236
    {
237
        $this->lockTry = max(1, (int) $number);
238
239
        return $this;
240
    }
241
242
    /**
243
     * @param float $float
244
     *
245
     * @return $this
246
     */
247
    public function setLockWait($float)
248
    {
249
        $this->lockWait = max(0.0001, $float);
250
251
        return $this;
252
    }
253
254
    /**
255
     * @return \Closure
256
     */
257
    protected function getWaitClosure()
258
    {
259
        if ($this->lockWait > 300) {
260
            $wait = (int) $this->lockWait;
261
262
            return function() use ($wait) {
263
                sleep($wait);
264
            };
265
        }
266
267
        $wait = (int) ($this->lockWait * 1000000);
268
269
        return function() use ($wait) {
270
            usleep($wait);
271
        };
272
    }
273
}
274