MediaWiki  master
MapCacheLRU.php
Go to the documentation of this file.
1 <?php
24 
39  private $cache = [];
41  private $timestamps = [];
43  private $epoch;
44 
46  private $maxCacheKeys;
47 
50 
52  const RANK_TOP = 1.0;
53 
55  const SIMPLE = 0;
57  const FIELDS = 1;
58 
63  public function __construct( $maxKeys ) {
64  Assert::parameterType( 'integer', $maxKeys, '$maxKeys' );
65  Assert::parameter( $maxKeys > 0, '$maxKeys', 'must be above zero' );
66 
67  $this->maxCacheKeys = $maxKeys;
68  // Use the current time as the default "as of" timestamp of entries
69  $this->epoch = $this->getCurrentTime();
70  }
71 
78  public static function newFromArray( array $values, $maxKeys ) {
79  $mapCache = new self( $maxKeys );
80  $mapCache->cache = ( count( $values ) > $maxKeys )
81  ? array_slice( $values, -$maxKeys, null, true )
82  : $values;
83 
84  return $mapCache;
85  }
86 
91  public function toArray() {
92  return $this->cache;
93  }
94 
110  public function set( $key, $value, $rank = self::RANK_TOP ) {
111  if ( $this->has( $key ) ) {
112  $this->ping( $key );
113  } elseif ( count( $this->cache ) >= $this->maxCacheKeys ) {
114  reset( $this->cache );
115  $evictKey = key( $this->cache );
116  unset( $this->cache[$evictKey] );
117  unset( $this->timestamps[$evictKey] );
118  }
119 
120  if ( $rank < 1.0 && $rank > 0 ) {
121  $offset = intval( $rank * count( $this->cache ) );
122  $this->cache = array_slice( $this->cache, 0, $offset, true )
123  + [ $key => $value ]
124  + array_slice( $this->cache, $offset, null, true );
125  } else {
126  $this->cache[$key] = $value;
127  }
128 
129  $this->timestamps[$key] = [
130  self::SIMPLE => $this->getCurrentTime(),
131  self::FIELDS => []
132  ];
133  }
134 
143  public function has( $key, $maxAge = INF ) {
144  if ( !is_int( $key ) && !is_string( $key ) ) {
145  throw new UnexpectedValueException(
146  __METHOD__ . ': invalid key; must be string or integer.' );
147  }
148 
149  if ( !array_key_exists( $key, $this->cache ) ) {
150  return false;
151  }
152 
153  return ( $maxAge <= 0 || $this->getAge( $key ) <= $maxAge );
154  }
155 
168  public function get( $key, $maxAge = INF, $default = null ) {
169  if ( !$this->has( $key, $maxAge ) ) {
170  return $default;
171  }
172 
173  $this->ping( $key );
174 
175  return $this->cache[$key];
176  }
177 
184  public function setField( $key, $field, $value, $initRank = self::RANK_TOP ) {
185  if ( $this->has( $key ) ) {
186  $this->ping( $key );
187  } else {
188  $this->set( $key, [], $initRank );
189  }
190 
191  if ( !is_int( $field ) && !is_string( $field ) ) {
192  throw new UnexpectedValueException(
193  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
194  }
195 
196  if ( !is_array( $this->cache[$key] ) ) {
197  $type = gettype( $this->cache[$key] );
198 
199  throw new UnexpectedValueException( "The value of '$key' ($type) is not an array." );
200  }
201 
202  $this->cache[$key][$field] = $value;
203  $this->timestamps[$key][self::FIELDS][$field] = $this->getCurrentTime();
204  }
205 
213  public function hasField( $key, $field, $maxAge = INF ) {
214  $value = $this->get( $key );
215 
216  if ( !is_int( $field ) && !is_string( $field ) ) {
217  throw new UnexpectedValueException(
218  __METHOD__ . ": invalid field for '$key'; must be string or integer." );
219  }
220 
221  if ( !is_array( $value ) || !array_key_exists( $field, $value ) ) {
222  return false;
223  }
224 
225  return ( $maxAge <= 0 || $this->getAge( $key, $field ) <= $maxAge );
226  }
227 
235  public function getField( $key, $field, $maxAge = INF ) {
236  if ( !$this->hasField( $key, $field, $maxAge ) ) {
237  return null;
238  }
239 
240  return $this->cache[$key][$field];
241  }
242 
247  public function getAllKeys() {
248  return array_keys( $this->cache );
249  }
250 
264  public function getWithSetCallback(
265  $key, callable $callback, $rank = self::RANK_TOP, $maxAge = INF
266  ) {
267  if ( $this->has( $key, $maxAge ) ) {
268  $value = $this->get( $key );
269  } else {
270  $value = call_user_func( $callback );
271  if ( $value !== false ) {
272  $this->set( $key, $value, $rank );
273  }
274  }
275 
276  return $value;
277  }
278 
285  public function clear( $keys = null ) {
286  if ( func_num_args() == 0 ) {
287  $this->cache = [];
288  $this->timestamps = [];
289  } else {
290  foreach ( (array)$keys as $key ) {
291  unset( $this->cache[$key] );
292  unset( $this->timestamps[$key] );
293  }
294  }
295  }
296 
303  public function getMaxSize() {
304  return $this->maxCacheKeys;
305  }
306 
315  public function setMaxSize( $maxKeys ) {
316  Assert::parameterType( 'integer', $maxKeys, '$maxKeys' );
317  Assert::parameter( $maxKeys > 0, '$maxKeys', 'must be above zero' );
318 
319  $this->maxCacheKeys = $maxKeys;
320  while ( count( $this->cache ) > $this->maxCacheKeys ) {
321  reset( $this->cache );
322  $evictKey = key( $this->cache );
323  unset( $this->cache[$evictKey] );
324  unset( $this->timestamps[$evictKey] );
325  }
326  }
327 
333  private function ping( $key ) {
334  $item = $this->cache[$key];
335  unset( $this->cache[$key] );
336  $this->cache[$key] = $item;
337  }
338 
344  private function getAge( $key, $field = null ) {
345  if ( $field !== null ) {
346  $mtime = $this->timestamps[$key][self::FIELDS][$field] ?? $this->epoch;
347  } else {
348  $mtime = $this->timestamps[$key][self::SIMPLE] ?? $this->epoch;
349  }
350 
351  return ( $this->getCurrentTime() - $mtime );
352  }
353 
354  public function serialize() {
355  return serialize( [
356  'entries' => $this->cache,
357  'timestamps' => $this->timestamps
358  ] );
359  }
360 
361  public function unserialize( $serialized ) {
362  $data = unserialize( $serialized );
363  $this->cache = $data['entries'] ?? [];
364  $this->timestamps = $data['timestamps'] ?? [];
365  $this->epoch = $this->getCurrentTime();
366  }
367 
372  protected function getCurrentTime() {
373  return $this->wallClockOverride ?: microtime( true );
374  }
375 
380  public function setMockTime( &$time ) {
381  $this->wallClockOverride =& $time;
382  }
383 }
getField( $key, $field, $maxAge=INF)
clear( $keys=null)
Clear one or several cache entries, or all cache entries.
__construct( $maxKeys)
Definition: MapCacheLRU.php:63
hasField( $key, $field, $maxAge=INF)
array $timestamps
Map of (key => (UNIX timestamp, (field => UNIX timestamp)))
Definition: MapCacheLRU.php:41
ping( $key)
Push an entry to the top of the cache.
getWithSetCallback( $key, callable $callback, $rank=self::RANK_TOP, $maxAge=INF)
Get an item with the given key, producing and setting it if not found.
has( $key, $maxAge=INF)
Check if a key exists.
float null $wallClockOverride
Definition: MapCacheLRU.php:49
array $cache
Map of (key => value)
Definition: MapCacheLRU.php:39
getAge( $key, $field=null)
unserialize( $serialized)
int $maxCacheKeys
Max number of entries.
Definition: MapCacheLRU.php:46
setField( $key, $field, $value, $initRank=self::RANK_TOP)
static newFromArray(array $values, $maxKeys)
Definition: MapCacheLRU.php:78
setMaxSize( $maxKeys)
Resize the maximum number of cache entries, removing older entries as needed.
getMaxSize()
Get the maximum number of keys allowed.
setMockTime(&$time)
foreach( $res as $row) $serialized
float $epoch
Default entry timestamp if not specified.
Definition: MapCacheLRU.php:43