1
|
|
|
<?php |
2
|
|
|
|
3
|
|
|
namespace Gielfeldt\Iterators; |
4
|
|
|
|
5
|
|
|
class AtomicTempFileObject extends \SplFileObject |
6
|
|
|
{ |
7
|
|
|
const DISCARD = 1; |
8
|
|
|
const PERSIST = 2; |
9
|
|
|
const PERSIST_UNCHANGED = 3; |
10
|
|
|
|
11
|
|
|
protected $destinationRealPath; |
12
|
|
|
protected $persist = 0; |
13
|
|
|
protected $onPersistCallback; |
14
|
|
|
protected $onDiscardCallback; |
15
|
|
|
protected $onCompareCallback; |
16
|
|
|
|
17
|
|
|
/** |
18
|
|
|
* Constructor. |
19
|
|
|
*/ |
20
|
21 |
|
public function __construct(string $filename, $mode = 0755) |
21
|
|
|
{ |
22
|
21 |
|
$tempDir = dirname($filename); |
23
|
21 |
|
if (!file_exists($tempDir)) { |
24
|
4 |
|
if (!@mkdir($tempDir, $mode, true)) { |
25
|
|
|
// @codeCoverageIgnoreStart |
26
|
|
|
$lastError = error_get_last(); |
27
|
|
|
throw new \RuntimeException( |
28
|
|
|
sprintf( |
29
|
|
|
"Could create directory %s - message: %s", |
30
|
|
|
$tempDir, |
31
|
|
|
$lastError['message'] |
32
|
|
|
) |
33
|
|
|
); |
34
|
|
|
// @codeCoverageIgnoreEnd |
35
|
|
|
} |
36
|
|
|
} |
37
|
21 |
|
$tempPrefix = basename($filename) . '.AtomicTempFileObject.'; |
38
|
21 |
|
$this->destinationRealPath = $filename; |
39
|
21 |
|
parent::__construct(tempnam($tempDir, $tempPrefix), "w+"); |
40
|
21 |
|
$this->onCompare([self::class, 'compare']); |
41
|
21 |
|
} |
42
|
|
|
|
43
|
|
|
/** |
44
|
|
|
* Get the destination real path. |
45
|
|
|
* |
46
|
|
|
* @return string |
47
|
|
|
* The real path of the destination. |
48
|
|
|
*/ |
49
|
15 |
|
public function getDestinationRealPath(): string |
50
|
|
|
{ |
51
|
15 |
|
return $this->destinationRealPath; |
52
|
|
|
} |
53
|
|
|
|
54
|
|
|
/** |
55
|
|
|
* Move temp file into the destination upon object desctruction. |
56
|
|
|
*/ |
57
|
20 |
|
public function persistOnClose($persist = self::PERSIST): AtomicTempFileObject |
58
|
|
|
{ |
59
|
20 |
|
$this->persist = $persist; |
60
|
20 |
|
return $this; |
61
|
|
|
} |
62
|
|
|
|
63
|
1 |
|
public function onPersist(callable $callback) |
64
|
|
|
{ |
65
|
1 |
|
$this->onPersistCallback = \Closure::fromCallable($callback); |
66
|
1 |
|
} |
67
|
|
|
|
68
|
1 |
|
public function onDiscard(callable $callback) |
69
|
|
|
{ |
70
|
1 |
|
$this->onDiscardCallback = \Closure::fromCallable($callback); |
71
|
1 |
|
} |
72
|
|
|
|
73
|
21 |
|
public function onCompare(callable $callback) |
74
|
|
|
{ |
75
|
21 |
|
$this->onCompareCallback = \Closure::fromCallable($callback); |
76
|
21 |
|
} |
77
|
|
|
|
78
|
12 |
|
private function doPersist() |
79
|
|
|
{ |
80
|
12 |
View Code Duplication |
if (!@rename($this->getRealPath(), $this->destinationRealPath)) { |
81
|
|
|
// @codeCoverageIgnoreStart |
82
|
|
|
$lastError = error_get_last(); |
83
|
|
|
throw new \RuntimeException( |
84
|
|
|
sprintf( |
85
|
|
|
"Could not move %s to %s - message: %s", |
86
|
|
|
$this->getRealPath(), |
87
|
|
|
$this->destinationRealPath, |
88
|
|
|
$lastError['message'] |
89
|
|
|
) |
90
|
|
|
); |
91
|
|
|
// @codeCoverageIgnoreEnd |
92
|
|
|
} |
93
|
12 |
|
if ($this->onPersistCallback) { |
94
|
1 |
|
($this->onPersistCallback)($this); |
95
|
|
|
} |
96
|
12 |
|
} |
97
|
|
|
|
98
|
12 |
|
private function doDiscard() |
99
|
|
|
{ |
100
|
12 |
View Code Duplication |
if (!@unlink($this->getRealPath())) { |
101
|
|
|
// @codeCoverageIgnoreStart |
102
|
|
|
$lastError = error_get_last(); |
103
|
|
|
throw new \RuntimeException( |
104
|
|
|
sprintf( |
105
|
|
|
"Could not remove %s - message: %s", |
106
|
|
|
$this->getRealPath(), |
107
|
|
|
$lastError['message'] |
108
|
|
|
) |
109
|
|
|
); |
110
|
|
|
// @codeCoverageIgnoreEnd |
111
|
|
|
} |
112
|
12 |
|
if ($this->onDiscardCallback) { |
113
|
1 |
|
($this->onDiscardCallback)($this); |
114
|
|
|
} |
115
|
12 |
|
} |
116
|
|
|
|
117
|
12 |
|
private function doCompare() |
118
|
|
|
{ |
119
|
12 |
|
return ($this->onCompareCallback)($this); |
120
|
|
|
} |
121
|
|
|
|
122
|
|
|
/** |
123
|
|
|
* Move temp file into the destination if applicable. |
124
|
|
|
*/ |
125
|
21 |
|
public function __destruct() |
126
|
|
|
{ |
127
|
21 |
|
$this->fflush(); |
128
|
21 |
|
if ($this->persist == self::PERSIST || $this->persist == self::PERSIST_UNCHANGED) { |
129
|
12 |
|
if ($this->persist == self::PERSIST_UNCHANGED || !$this->doCompare()) { |
130
|
12 |
|
$this->doPersist(); |
131
|
|
|
} else { |
132
|
2 |
|
$this->doDiscard(); |
133
|
|
|
} |
134
|
11 |
|
} elseif ($this->persist & self::DISCARD) { |
135
|
10 |
|
$this->doDiscard(); |
136
|
|
|
} else { |
137
|
|
|
// @codeCoverageIgnoreStart |
138
|
|
|
trigger_error("Temp file left on device: " . $this->getRealPath(), E_USER_WARNING); |
139
|
|
|
// @codeCoverageIgnoreEnd |
140
|
|
|
} |
141
|
20 |
|
} |
142
|
|
|
|
143
|
|
|
/** |
144
|
|
|
* Easy access iterator apply for processing an entire file. |
145
|
|
|
* |
146
|
|
|
* @param \Iterator $input [description] |
147
|
|
|
* @param callable $callback [description] |
148
|
|
|
*/ |
149
|
1 |
View Code Duplication |
public function process(\Iterator $input, callable $callback) |
|
|
|
|
150
|
|
|
{ |
151
|
1 |
|
$callback = \Closure::fromCallable($callback); |
152
|
1 |
|
$input->rewind(); |
153
|
1 |
|
iterator_apply( |
154
|
|
|
$input, |
155
|
1 |
|
function (\Iterator $iterator) use ($callback) { |
156
|
1 |
|
$callback($iterator->current(), $iterator->key(), $iterator, $this); |
157
|
1 |
|
return true; |
158
|
1 |
|
}, |
159
|
1 |
|
[$input] |
160
|
|
|
); |
161
|
1 |
|
return $this; |
162
|
|
|
} |
163
|
|
|
|
164
|
|
|
/** |
165
|
|
|
* Atomic file_put_contents(). |
166
|
|
|
* |
167
|
|
|
* @see file_put_contents() |
168
|
|
|
*/ |
169
|
1 |
|
public static function file_put_contents($filename, $data, $flags = 0) |
170
|
|
|
{ |
171
|
1 |
|
if ($flags & FILE_USE_INCLUDE_PATH) { |
172
|
1 |
|
$file = (new \SplFileInfo($filename))->openFile('r', true); |
173
|
1 |
|
if ($file) { |
174
|
1 |
|
$filename = $file->getRealPath(); |
175
|
|
|
} |
176
|
|
|
} |
177
|
1 |
|
$tempFile = new static($filename); |
178
|
1 |
|
$tempFile->fwrite($data); |
179
|
1 |
|
$tempFile->persistOnClose(); |
180
|
1 |
|
unset($tempFile); |
181
|
1 |
|
} |
182
|
|
|
|
183
|
|
|
/** |
184
|
|
|
* File comparison |
185
|
|
|
* |
186
|
|
|
* @return bool |
187
|
|
|
* True if the contents of this file matches the contents of $filename. |
188
|
|
|
*/ |
189
|
12 |
|
private static function compare(AtomicTempFileObject $tempFile): bool |
190
|
|
|
{ |
191
|
12 |
|
$filename = $tempFile->getDestinationRealPath(); |
192
|
12 |
|
if (!file_exists($filename)) { |
193
|
12 |
|
return false; |
194
|
|
|
} |
195
|
|
|
|
196
|
|
|
// This is a temp file opened for writing and truncated to begin with, |
197
|
|
|
// so we assume that the current position is the size of the new file. |
198
|
4 |
|
$pos = $tempFile->ftell(); |
199
|
|
|
|
200
|
4 |
|
$file = new \SplFileObject($filename, 'r'); |
201
|
4 |
|
if ($pos <> $file->getSize()) { |
202
|
1 |
|
return false; |
203
|
|
|
} |
204
|
|
|
|
205
|
|
|
// Rewind this temp file and compare it with the specified file. |
206
|
4 |
|
$identical = true; |
207
|
4 |
|
$tempFile->fseek(0); |
208
|
4 |
|
while (!$file->eof()) { |
209
|
4 |
|
if ($file->fread(8192) != $tempFile->fread(8192)) { |
210
|
3 |
|
$identical = false; |
211
|
3 |
|
break; |
212
|
|
|
} |
213
|
|
|
} |
214
|
|
|
|
215
|
|
|
// Reset file pointer to end of file. |
216
|
4 |
|
$tempFile->fseek($pos); |
217
|
4 |
|
return $identical; |
218
|
|
|
} |
219
|
|
|
} |
220
|
|
|
|
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.