1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace ICanBoogie;
13
14 15 16
17 class Vars implements \ArrayAccess, \IteratorAggregate
18 {
19 20 21 22 23
24 const MAGIC = "VAR\0SLZ\0";
25
26 27 28 29 30
31 const MAGIC_LENGTH = 8;
32
33 static private $release_after;
34
35 36 37 38 39
40 protected $path;
41
42 43 44 45 46 47 48
49 public function __construct($path)
50 {
51 $this->path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
52
53 if (self::$release_after === null)
54 {
55 self::$release_after = strpos(PHP_OS, 'WIN') === 0 ? false : true;
56 }
57
58 if (!is_writable($this->path))
59 {
60 throw new Exception('The directory %directory is not writable.', [ 'directory' => $this->path ]);
61 }
62 }
63
64 65 66 67 68
69 public function offsetSet($name, $value)
70 {
71 $this->store($name, $value);
72 }
73
74 75 76 77 78 79 80 81 82
83 public function offsetExists($name)
84 {
85 $pathname = $this->path . $name;
86
87 return file_exists($pathname);
88 }
89
90 91 92 93 94
95 public function offsetUnset($name)
96 {
97 $pathname = $this->path . $name;
98
99 if (!file_exists($pathname))
100 {
101 return;
102 }
103
104 unlink($pathname);
105 }
106
107 108 109 110 111
112 public function offsetGet($name)
113 {
114 return $this->retrieve($name);
115 }
116
117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
133 public function store($key, $value, $ttl=0)
134 {
135 $pathname = $this->path . $key;
136 $ttl_mark = $pathname . '.ttl';
137
138 if ($ttl)
139 {
140 $future = time() + $ttl;
141
142 touch($ttl_mark, $future, $future);
143 }
144 else if (file_exists($ttl_mark))
145 {
146 unlink($ttl_mark);
147 }
148
149 $dir = dirname($pathname);
150
151 if (!file_exists($dir))
152 {
153 mkdir($dir, 0705, true);
154 }
155
156 $uniq_id = uniqid(mt_rand(), true);
157 $tmp_pathname = $dir . '/var-' . $uniq_id;
158 $garbage_pathname = $dir . '/garbage-var-' . $uniq_id;
159
160
161
162
163
164
165 if (is_array($value) || is_object($value))
166 {
167 $value = self::MAGIC . serialize($value);
168 }
169
170 if ($value === true)
171 {
172 touch($pathname);
173 }
174 else if ($value === false || $value === null)
175 {
176 $this->offsetUnset($key);
177 }
178 else
179 {
180
181
182
183
184
185 $fh = fopen($pathname, 'a+');
186
187 if (!$fh)
188 {
189 throw new Exception('Unable to open %pathname: :message', [ 'pathname' => $pathname, 'message' => Debug::$last_error_message ]);
190 }
191
192 if (self::$release_after && !flock($fh, LOCK_EX))
193 {
194 throw new Exception('Unable to get to exclusive lock on %pathname: :message', [ 'pathname' => $pathname, 'message' => Debug::$last_error_message ]);
195 }
196
197 file_put_contents($tmp_pathname, $value);
198
199
200
201
202 if (!self::$release_after)
203 {
204 fclose($fh);
205 }
206
207 if (!rename($pathname, $garbage_pathname))
208 {
209 throw new Exception('Unable to rename %old as %new.', [ 'old' => $pathname, 'new' => $garbage_pathname ]);
210 }
211
212 if (!rename($tmp_pathname, $pathname))
213 {
214 throw new Exception('Unable to rename %old as %new.', [ 'old' => $tmp_pathname, 'new' => $pathname ]);
215 }
216
217 if (!unlink($garbage_pathname))
218 {
219 throw new Exception('Unable to delete %pathname: :message', [ 'pathname' => $garbage_pathname, 'message' => Debug::$last_error_message ]);
220 }
221
222 if (self::$release_after)
223 {
224 flock($fh, LOCK_UN);
225 fclose($fh);
226 }
227 }
228 }
229
230 231 232 233 234 235 236 237 238 239
240 public function retrieve($name, $default=null)
241 {
242 $pathname = $this->path . $name;
243 $ttl_mark = $pathname . '.ttl';
244
245 if (file_exists($ttl_mark) && fileatime($ttl_mark) < time() || !file_exists($pathname))
246 {
247 return $default;
248 }
249
250 $value = file_get_contents($pathname);
251
252 if (substr($value, 0, self::MAGIC_LENGTH) == self::MAGIC)
253 {
254 $value = unserialize(substr($value, self::MAGIC_LENGTH));
255 }
256
257 return $value;
258 }
259
260 261 262 263 264 265 266
267 public function getIterator()
268 {
269 $iterator = new \DirectoryIterator($this->path);
270
271 return new VarsIterator($iterator);
272 }
273
274 275 276 277 278 279 280
281 public function matching($regex)
282 {
283 $dir = new \DirectoryIterator($this->path);
284 $filter = new \RegexIterator($dir, $regex);
285
286 return new VarsIterator($filter);
287 }
288 }
289
290 291 292 293 294 295 296
297 class VarsIterator implements \Iterator
298 {
299 300 301 302 303
304 protected $iterator;
305
306 public function __construct(\Iterator $iterator)
307 {
308 $this->iterator = $iterator;
309 }
310
311 312 313 314 315 316 317
318 public function current()
319 {
320 $file = $this->iterator->current();
321
322 if ($file->isDot())
323 {
324 $this->iterator->next();
325
326 $file = $this->current();
327 }
328
329 return $file;
330 }
331
332 public function next()
333 {
334 $this->iterator->next();
335 }
336
337 338 339 340 341
342 public function key()
343 {
344 return $this->iterator->current()->getFilename();
345 }
346
347 public function valid()
348 {
349 return $this->iterator->valid();
350 }
351
352 public function rewind()
353 {
354 $this->iterator->rewind();
355 }
356
357 358 359
360 public function delete()
361 {
362 foreach ($this->iterator as $file)
363 {
364 unlink($file->getPathname());
365 }
366 }
367 }